diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 1dde4f60b5..dbe604a60c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -11,6 +11,7 @@ diff --git a/app/build.gradle b/app/build.gradle index ec8de2db1a..d6bf56e099 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,6 +44,7 @@ android { } dependencies { + implementation project(':ripple-core') implementation project(':card-common') implementation project(':card-android') implementation project(':server-android') diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index e229b98330..31e89d72ee 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -12,12 +12,13 @@ public enum Blockchain { Ethereum("ETH", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum"), EthereumTestNet("ETH/test", "ETH", 1.0, R.drawable.ic_logo_ethereum_testnet, "Ethereum Testnet"), Token("Token", "ETH", 1.0, R.drawable.ic_logo_bat_token, "Ethereum"), - NftToken("NftToken", "", 1.0, R.drawable.ic_logo_bat_token, "Ethereum"), + NftToken("NftToken", "", 1.0, R.drawable.tangem2, "Ethereum"), BitcoinCash("BCH", "BCH", 100000000.0, R.drawable.ic_logo_bitcoin_cash, "Bitcoin Cash"), - Litecoin("LTC", "LTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Litecoin"), - Rootstock("RSK", "RBTC", 1.0, R.drawable.ic_logo_bitcoin, "Rootstock"), - RootstockToken("Token", "RBTC", 1.0, R.drawable.ic_logo_bat_token, "Rootstock"), - Cardano("CARDANO", "ADA", 1000000.0,R.drawable.ic_logo_bitcoin, "Cardano"); + Litecoin("LTC", "LTC", 100000000.0, R.drawable.tangem2, "Litecoin"), + Rootstock("RSK", "RBTC", 1.0, R.drawable.tangem2, "Rootstock"), + RootstockToken("Token", "RBTC", 1.0, R.drawable.tangem2, "Rootstock"), + Cardano("CARDANO", "ADA", 1000000.0, R.drawable.tangem2, "Cardano"), + Ripple ("XRP", "XRP", 1000000.0, R.drawable.tangem2, "Ripple"); Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; @@ -92,6 +93,9 @@ public enum Blockchain { case "BTC": return R.drawable.ic_logo_bitcoin; + case "BCH": + return R.drawable.ic_logo_bitcoin_cash; + case "Token": if (symbolName.equals("SEED")) return R.drawable.ic_logo_seed; diff --git a/app/src/main/java/com/tangem/data/network/RippleApi.java b/app/src/main/java/com/tangem/data/network/RippleApi.java new file mode 100644 index 0000000000..1d457ad859 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/RippleApi.java @@ -0,0 +1,15 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.RippleBody; +import com.tangem.data.network.model.RippleResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface RippleApi { + @Headers("Content-Type: application/json") + @POST + Call ripple(@Body RippleBody body); +} diff --git a/app/src/main/java/com/tangem/data/network/ServerApiRipple.java b/app/src/main/java/com/tangem/data/network/ServerApiRipple.java new file mode 100644 index 0000000000..ba5c356b47 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiRipple.java @@ -0,0 +1,114 @@ +package com.tangem.data.network; + +import android.util.Log; + +import com.tangem.App; +import com.tangem.data.network.model.RippleBody; +import com.tangem.data.network.model.RippleResponse; + +import java.util.HashMap; +import java.util.Map; + +import androidx.annotation.NonNull; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ServerApiRipple { + private static String TAG = ServerApiRipple.class.getSimpleName(); + + public static final String RIPPLE_ACCOUNT_INFO = "account_info"; + public static final String RIPPLE_ACCOUNT_UNCONFIRMED = "account_unconfirmed"; + public static final String RIPPLE_SUBMIT = "submit"; + public static final String RIPPLE_FEE = "fee"; + + private int requestsCount=0; + + public static String lastNode; + + public boolean isRequestsSequenceCompleted() { + Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount)); + return requestsCount <= 0; + } + + private ResponseListener responseListener; + + public interface ResponseListener { + void onSuccess(String method, RippleResponse infuraResponse); + + void onFail(String method, String message); + } + + public void setResponseListener(ResponseListener listener) { + responseListener = listener; + } + + public void requestData(String method, int id, String wallet, String tx) { + requestsCount++; + String rippleURL = "http://s1.ripple.com:51234/"; //TODO: make random selection + lastNode = rippleURL; //TODO: show node instead of URL + + Retrofit retrofitRipple = new Retrofit.Builder() + .baseUrl(rippleURL) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + RippleApi rippleApi = retrofitRipple.create(RippleApi.class); + + RippleBody rippleBody; + HashMap paramsMap; + + switch (method) { + case RIPPLE_ACCOUNT_INFO: + paramsMap = new HashMap<>(); + paramsMap.put("account", wallet); + paramsMap.put("ledger_index", "validated"); + rippleBody = new RippleBody(method, paramsMap); + break; + + case RIPPLE_ACCOUNT_UNCONFIRMED: + paramsMap = new HashMap<>(); + paramsMap.put("account", wallet); + paramsMap.put("ledger_index", "current"); +// paramsMap.put("queue", "true"); TODO: make queue check if needed + rippleBody = new RippleBody(RIPPLE_ACCOUNT_INFO, paramsMap); + break; + + case RIPPLE_FEE: + rippleBody = new RippleBody(method, new HashMap<>()); + break; + + case RIPPLE_SUBMIT: + paramsMap = new HashMap<>(); + paramsMap.put("tx_blob", tx); + rippleBody = new RippleBody(method, paramsMap); + break; + + default: + rippleBody = new RippleBody(); + } + + Call call = rippleApi.ripple(rippleBody); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.code() == 200) { + requestsCount--; + responseListener.onSuccess(method, response.body()); + Log.i(TAG, "requestData " + method + " onResponse " + response.code()); + } else { + responseListener.onFail(method, String.valueOf(response.code())); + Log.e(TAG, "requestData " + method + " onResponse " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + responseListener.onFail(method, String.valueOf(t.getMessage())); + Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage()); + } + }); + } +} diff --git a/app/src/main/java/com/tangem/data/network/model/RippleBody.java b/app/src/main/java/com/tangem/data/network/model/RippleBody.java new file mode 100644 index 0000000000..0423f357a1 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/RippleBody.java @@ -0,0 +1,24 @@ +package com.tangem.data.network.model; + +import java.util.ArrayList; +import java.util.HashMap; + +public class RippleBody { + private String method; + private ArrayList> params; + + public RippleBody() { + } + + //for RIPPLE_FEE + public RippleBody(String method) { + this.method = method; + } + + public RippleBody(String method, HashMap paramsMap) { + this.method = method; + ArrayList> paramsList = new ArrayList<>(); + paramsList.add(paramsMap); + this.params = paramsList; + } +} diff --git a/app/src/main/java/com/tangem/data/network/model/RippleResponse.kt b/app/src/main/java/com/tangem/data/network/model/RippleResponse.kt new file mode 100644 index 0000000000..6c61b6abb0 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/RippleResponse.kt @@ -0,0 +1,52 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class RippleResponse( + @SerializedName("result") + var result: RippleResult? = null +) + +data class RippleResult( + @SerializedName("account_data") + var account_data: RippleAccountData? = null, + + @SerializedName("validated") + var validated: Boolean? = null, + + //for RIPPLE_FEE + @SerializedName("drops") + var drops: FeeDrops? = null, + + //for RIPPLE_SUBMIT + @SerializedName("engine_result") + var engine_result: String? = null, + + //for RIPPLE_SUBMIT + @SerializedName ("engine_result_message") + var engine_result_message: String? = null +) + +data class RippleAccountData( + @SerializedName("Account") + var account: String? = null, + + @SerializedName("Balance") + var balance: String? = null, + + @SerializedName("Sequence") + var sequence: Int? = null +) + +data class FeeDrops( + //enough to put tx to queue + @SerializedName("minimum_fee") + var minimum_fee: String? = null, + + //enough to put tx to current ledger + @SerializedName("open_ledger_fee") + var open_ledger_fee: String? = null, + + @SerializedName("median_fee") + var median_fee: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java new file mode 100644 index 0000000000..7a34b0d289 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java @@ -0,0 +1,115 @@ +package com.tangem.domain.wallet.xrp; + +import java.math.BigInteger; + +/** + * Created by Ilia on 15.02.2018. + */ + +public class XrpBase58 { + private static final char[] BASE58 = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz".toCharArray(); + + private static final int BASE58_CHUNK_DIGITS = 10;//how many base 58 digits fits in long + private static final BigInteger BASE58_CHUNK_MOD = BigInteger.valueOf(0x5fa8624c7fba400L); //58^BASE58_CHUNK_DIGITS + private static final byte[] BASE58_VALUES = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, + -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1, + -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + + public static byte[] decodeBase58(String input) { + if (input == null) { + return null; + } + input = input.trim(); + if (input.length() == 0) { + return new byte[0]; + } + BigInteger resultNum = BigInteger.ZERO; + int nLeadingZeros = 0; + while (nLeadingZeros < input.length() && input.charAt(nLeadingZeros) == BASE58[0]) { + nLeadingZeros++; + } + long acc = 0; + int nDigits = 0; + int p = nLeadingZeros; + while (p < input.length()) { + int v = BASE58_VALUES[input.charAt(p) & 0xff]; + if (v >= 0) { + acc *= 58; + acc += v; + nDigits++; + if (nDigits == BASE58_CHUNK_DIGITS) { + resultNum = resultNum.multiply(BASE58_CHUNK_MOD).add(BigInteger.valueOf(acc)); + acc = 0; + nDigits = 0; + } + p++; + } else { + break; + } + } + if (nDigits > 0) { + long mul = 58; + while (--nDigits > 0) { + mul *= 58; + } + resultNum = resultNum.multiply(BigInteger.valueOf(mul)).add(BigInteger.valueOf(acc)); + } + final int BASE58_SPACE = -2; + while (p < input.length() && BASE58_VALUES[input.charAt(p) & 0xff] == BASE58_SPACE) { + p++; + } + if (p < input.length()) { + return null; + } + byte[] plainNumber = resultNum.toByteArray(); + int plainNumbersOffs = plainNumber[0] == 0 ? 1 : 0; + byte[] result = new byte[nLeadingZeros + plainNumber.length - plainNumbersOffs]; + System.arraycopy(plainNumber, plainNumbersOffs, result, nLeadingZeros, plainNumber.length - plainNumbersOffs); + return result; + } + + public static String encodeBase58(byte[] input) { + if (input == null) { + return null; + } + StringBuilder str = new StringBuilder((input.length * 350) / 256 + 1); + BigInteger bn = new BigInteger(1, input); + long rem; + while (true) { + BigInteger[] divideAndRemainder = bn.divideAndRemainder(BASE58_CHUNK_MOD); + bn = divideAndRemainder[0]; + rem = divideAndRemainder[1].longValue(); + if (bn.compareTo(BigInteger.ZERO) == 0) { + break; + } + for (int i = 0; i < BASE58_CHUNK_DIGITS; i++) { + str.append(BASE58[(int) (rem % 58)]); + rem /= 58; + } + } + while (rem != 0) { + str.append(BASE58[(int) (rem % 58)]); + rem /= 58; + } + str.reverse(); + int nLeadingZeros = 0; + while (nLeadingZeros < input.length && input[nLeadingZeros] == 0) { + str.insert(0, BASE58[0]); + nLeadingZeros++; + } + return str.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java new file mode 100644 index 0000000000..64613e1605 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java @@ -0,0 +1,78 @@ +package com.tangem.domain.wallet.xrp; + +import android.os.Bundle; +import android.util.Log; + +import com.tangem.domain.wallet.CoinData; +import com.tangem.domain.wallet.CoinEngine; + +import java.math.BigDecimal; + +public class XrpData extends CoinData { + public XrpData() { + super(); + } + + private Long balanceConfirmed, balanceUnconfirmed, sequence; + + @Override + public void loadFromBundle(Bundle B) { + super.loadFromBundle(B); + + if (B.containsKey("BalanceConfirmed")) balanceConfirmed = B.getLong("BalanceConfirmed"); + else balanceConfirmed = null; + if (B.containsKey("BalanceUnconfirmed")) balanceUnconfirmed = B.getLong("BalanceUnconfirmed"); + else balanceUnconfirmed = null; + if (B.containsKey("Sequence")) sequence = B.getLong("Sequence"); + } + + @Override + public void saveToBundle(Bundle B) { + super.saveToBundle(B); + try { + if (balanceConfirmed != null) B.putLong("BalanceConfirmed", balanceConfirmed); + if (balanceUnconfirmed != null) B.putLong("BalanceUnconfirmed", balanceUnconfirmed); + if (sequence != null) B.putLong("Sequence", sequence); + } catch (Exception e) { + Log.e("Can't save to bundle ", e.getMessage()); + } + } + + @Override + public void clearInfo() { + super.clearInfo(); + balanceConfirmed = null; + balanceUnconfirmed = null; + sequence = null; + } + + // balanceUnconfirmed is just the latest balance, it equals balanceConfirmed if no unconfirmed transaction present + public CoinEngine.InternalAmount getBalanceInInternalUnits() { + return new CoinEngine.InternalAmount(BigDecimal.valueOf(balanceUnconfirmed), "Drops"); + } + +// public Long getBalanceUnconfirmed() { +// return balanceUnconfirmed; +// } + + public void setBalanceConfirmed(Long balance) { + this.balanceConfirmed = balance; + } + + public void setBalanceUnconfirmed(Long balance) { + this.balanceUnconfirmed = balance; + } + + public void setSequence(Long sequence) { + this.sequence = sequence; + } + + public boolean hasBalanceInfo() { + return balanceConfirmed != null || balanceUnconfirmed != null; + } + + public boolean hasUnconfirmed() { + return !balanceConfirmed.equals(balanceUnconfirmed); + } + +} diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java new file mode 100644 index 0000000000..53ef3e3162 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java @@ -0,0 +1,411 @@ +package com.tangem.domain.wallet.xrp; + +import android.net.Uri; +import android.text.InputFilter; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.types.known.tx.signed.SignedTransaction; +import com.ripple.core.types.known.tx.txns.Payment; +import com.tangem.card_common.data.TangemCard; +import com.tangem.card_common.reader.CardProtocol; +import com.tangem.card_common.tasks.SignTask; +import com.tangem.card_common.util.Util; +import com.tangem.domain.wallet.BTCUtils; +import com.tangem.domain.wallet.BalanceValidator; +import com.tangem.domain.wallet.CoinData; +import com.tangem.domain.wallet.CoinEngine; +import com.tangem.domain.wallet.TangemContext; +import com.tangem.util.CryptoUtil; +import com.tangem.util.DecimalDigitsInputFilter; +import com.tangem.wallet.R; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Arrays; + +public class XrpEngine extends CoinEngine { + + private static final String TAG = XrpEngine.class.getSimpleName(); + + public XrpData coinData = null; + + public XrpEngine(TangemContext context) throws Exception { + super(context); + if (context.getCoinData() == null) { + coinData = new XrpData(); + context.setCoinData(coinData); + } else if (context.getCoinData() instanceof XrpData) { + coinData = (XrpData) context.getCoinData(); + } else { + throw new Exception("Invalid type of Blockchain data for XrpEngine"); + } + } + + public XrpEngine() { + super(); + } + + private static int getDecimals() { + return 6; + } + + private void checkBlockchainDataExists() throws Exception { + if (coinData == null) throw new Exception("No blockchain data"); + } + + @Override + public boolean awaitingConfirmation() { + if (coinData == null) return false; + return coinData.hasUnconfirmed(); + } + + @Override + public String getBalanceHTML() { + Amount balance = getBalance(); + if (balance != null) { + return balance.toDescriptionString(getDecimals()); + } else { + return ""; + } + } + + @Override + public String getBalanceCurrency() { + return "XRP"; + } + + @Override + public String getOfflineBalanceHTML() { + InternalAmount offlineInternalAmount = convertToInternalAmount(ctx.getCard().getOfflineBalance()); + Amount offlineAmount = convertToAmount(offlineInternalAmount); + return offlineAmount.toDescriptionString(getDecimals()); + } + + @Override + public boolean isBalanceNotZero() { + if (coinData == null) return false; + if (coinData.getBalanceInInternalUnits() == null) return false; + return coinData.getBalanceInInternalUnits().notZero(); + } + + @Override + public boolean hasBalanceInfo() { + if (coinData == null) return false; + return coinData.hasBalanceInfo(); + } + + public boolean isExtractPossible() { + if (!hasBalanceInfo()) { + ctx.setMessage(R.string.cannot_obtain_data_from_blockchain); + } else if (!isBalanceNotZero()) { + ctx.setMessage(R.string.wallet_empty); + } else if (awaitingConfirmation()) { + ctx.setMessage(R.string.please_wait_while_previous); + } else { + return true; + } + return false; + } + + @Override + public String getFeeCurrency() { + return "XRP"; + } + + @Override + public boolean validateAddress(String address) { + if (address == null || address.isEmpty()) { + return false; + } + + if (address.length() < 25) { + return false; + } + + if (address.length() > 35) { + return false; + } + + if (!address.startsWith("r")) { + return false; + } + + byte[] decAddress = XrpBase58.decodeBase58(address); + + if (decAddress == null || decAddress.length == 0) { + return false; + } + + byte[] payload = new byte[21]; + System.arraycopy(decAddress, 0, payload, 0, 21); + + byte[] checksum = new byte[4]; + System.arraycopy(decAddress, 21, checksum, 0, 4); + + byte[] calcChecksum = new byte[4]; + System.arraycopy(CryptoUtil.doubleSha256(payload),0,calcChecksum,0,4); + + if (!Arrays.equals(checksum, calcChecksum)) { + return false; + } + + return true; + } + + @Override + public boolean isNeedCheckNode() { + return true; + } + + @Override + public Uri getWalletExplorerUri() { + return Uri.parse("https://xrpscan.com/account/" + ctx.getCoinData().getWallet()); + } + + @Override + public InputFilter[] getAmountInputFilters() { + return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())}; + } + + @Override + public boolean checkNewTransactionAmount(Amount amount) { + if (coinData == null) return false; + if (amount.compareTo(convertToAmount(coinData.getBalanceInInternalUnits())) > 0) { + return false; + } + return true; + } + + @Override + public boolean checkNewTransactionAmountAndFee(Amount amountValue, Amount feeValue, Boolean isIncludeFee) { + InternalAmount fee; + InternalAmount amount; + + try { + checkBlockchainDataExists(); + amount = convertToInternalAmount(amountValue); + fee = convertToInternalAmount(feeValue); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + if (fee == null || amount == null) + return false; + + if (fee.isZero() || amount.isZero()) + return false; + + if (isIncludeFee && (amount.compareTo(coinData.getBalanceInInternalUnits()) > 0 || amount.compareTo(fee) < 0)) + return false; + + if (!isIncludeFee && amount.add(fee).compareTo(coinData.getBalanceInInternalUnits()) > 0) + return false; + + return true; + } + + @Override + public boolean validateBalance(BalanceValidator balanceValidator) { + try { + if (((ctx.getCard().getOfflineBalance() == null) && !ctx.getCoinData().isBalanceReceived()) || (!ctx.getCoinData().isBalanceReceived() && (ctx.getCard().getRemainingSignatures() != ctx.getCard().getMaxSignatures()))) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine("Unknown balance"); + balanceValidator.setSecondLine("Balance cannot be verified. Swipe down to refresh."); + return false; + } + + if (coinData.hasUnconfirmed()) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine("Transaction in progress"); + balanceValidator.setSecondLine("Wait for confirmation in blockchain"); + return false; + } + + if (coinData.isBalanceReceived()) {// && coinData.isBalanceEqual()) { TODO:check + balanceValidator.setScore(100); + balanceValidator.setFirstLine("Verified balance"); + balanceValidator.setSecondLine("Balance confirmed in blockchain"); + if (coinData.getBalanceInInternalUnits().isZero()) { + balanceValidator.setFirstLine("Empty wallet"); + balanceValidator.setSecondLine(""); + } + } + + if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) { + balanceValidator.setScore(80); + balanceValidator.setFirstLine("Verified offline balance"); + balanceValidator.setSecondLine("Can't obtain balance from blockchain. Restore internet connection to be more confident. "); + } + + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public Amount getBalance() { + if (!hasBalanceInfo()) return null; + return convertToAmount(coinData.getBalanceInInternalUnits()); + } + + @Override + public String evaluateFeeEquivalent(String fee) { + if (!coinData.getAmountEquivalentDescriptionAvailable()) return ""; + try { + Amount feeAmount = new Amount(fee, getFeeCurrency()); + return feeAmount.toEquivalentString(coinData.getRate()); + } catch (Exception e) { + return ""; + } + } + + @Override + public String getBalanceEquivalent() { + if (coinData == null || !coinData.getAmountEquivalentDescriptionAvailable()) return ""; + Amount balance = getBalance(); + if (balance == null) return ""; + return balance.toEquivalentString(coinData.getRate()); + } + + public String calculateAddress(byte[] pkCompressed) throws NoSuchAlgorithmException, NoSuchProviderException, IOException { + byte[] pkForHash = canonicalPubBytes(pkCompressed); + + ByteArrayOutputStream address = new ByteArrayOutputStream(); + address.write((byte) 0x00); + byte[] accountId = CryptoUtil.sha256ripemd160(pkForHash); + address.write(accountId); + + byte [] doubleSha256 = CryptoUtil.doubleSha256(address.toByteArray()); + address.write(Arrays.copyOfRange(doubleSha256,0,4)); + + return BTCUtils.toHex(address.toByteArray()); + } + + @Override + public Amount convertToAmount(InternalAmount internalAmount) { + BigDecimal d = internalAmount.divide(new BigDecimal("1000000")); + return new Amount(d, getBalanceCurrency()); + } + + @Override + public Amount convertToAmount(String strAmount, String currency) { + return new Amount(strAmount, currency); + } + + @Override + public InternalAmount convertToInternalAmount(Amount amount) { + BigDecimal d = amount.multiply(new BigDecimal("1000000")); + return new InternalAmount(d, "Drops"); + } + + @Override + public InternalAmount convertToInternalAmount(byte[] bytes) { + if (bytes == null) return null; + byte[] reversed = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) reversed[i] = bytes[bytes.length - i - 1]; + return new InternalAmount(Util.byteArrayToLong(reversed), "Drops"); + } + + @Override + public byte[] convertToByteArray(InternalAmount internalAmount) { + byte[] bytes = Util.longToByteArray(internalAmount.longValueExact()); +// byte[] reversed = new byte[bytes.length]; TODO: check if needed +// for (int i = 0; i < bytes.length; i++) reversed[i] = bytes[bytes.length - i - 1]; +// return reversed; + return bytes; + } + + @Override + public CoinData createCoinData() { + return new XrpData(); + } + + @Override + public String getUnspentInputsDescription() { + return ""; + } + + @Override + public void defineWallet() throws CardProtocol.TangemException { + try { + String wallet = calculateAddress(ctx.getCard().getWalletPublicKeyRar()); + ctx.getCoinData().setWallet(wallet); + } + catch (Exception e) + { + ctx.getCoinData().setWallet("ERROR"); + throw new CardProtocol.TangemException("Can't define wallet address"); + } + } + + public byte[] canonicalPubBytes(byte[] pkCompressed) { + byte[] pkForHash = new byte[33]; + + if (pkCompressed.length == 32) { + pkForHash[0] = (byte) 0xED; + System.arraycopy(pkCompressed, 0, pkForHash, 1,32); + } else { + pkForHash = pkCompressed; + } + } +} + + @Override + public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { + checkBlockchainDataExists(); + + Payment payment = new Payment(); + + // Put `as` AccountID field Account, `Object` o + payment.as(AccountID.Account, "rGZG674DSZJfoY8abMPSgChxZTJZEhyMRm"); + payment.as(AccountID.Destination, "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK"); + payment.as(com.ripple.core.coretypes.Amount.Amount, "1000000000"); + payment.as(UInt32.Sequence, 10); + payment.as(com.ripple.core.coretypes.Amount.Fee, "10000"); + + SignedTransaction signedTx = payment.prepare(canonicalPubBytes(ctx.getCard().getWalletPublicKeyRar())); + + return new SignTask.TransactionToSign() { + + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod == TangemCard.SigningMethod.Sign_Hash; + } + + @Override + public byte[][] getHashesToSign() { + byte[][] dataForSign = new byte[1][]; + dataForSign[0] = signedTx.signingData; + return dataForSign; + } + + @Override + public byte[] getRawDataToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); + } + + @Override + public String getHashAlgToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); + } + + @Override + public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception { + throw new Exception("Transaction validation by issuer not supported in this version"); + } + + @Override + public byte[] onSignCompleted(byte[] signFromCard) { + signedTx.addSign(signFromCard); + return BTCUtils.fromHex(signedTx.tx_blob); + } + }; + } +} diff --git a/ripple-core/.gitignore b/ripple-core/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/ripple-core/.gitignore @@ -0,0 +1 @@ +/target diff --git a/ripple-core/README.md b/ripple-core/README.md new file mode 100644 index 0000000000..d89d96f5e8 --- /dev/null +++ b/ripple-core/README.md @@ -0,0 +1,391 @@ +# SerializedTypes Overview + +Ripple uses a node store, where objects are keyed by a 32byte hash value. + +These keys are created by hashing a binary representation of the whole object +prefixed with a name-spacing sequence of bytes, unique to each class of object. +As such, a method of consistently producing a binary sequence from a given +object was required. + +Objects have fields, which are a pairing of a type and a name. Names are simply +ordinals used to look up preassigned names in a per type table. The type ordinal, +similarly, is used to lookup a given class. + +In ripple-lib-java the type equates to a `SerializedType`, with types such as 32 +bit unsigned integer, variable length byte strings and even arrays of objects. +The fields have an ordinal quality, and can be deterministically sorted, +important for consistent binary representation. + +There are also container types: An STObject, short for `SerializedType Object`, +is an associative container of fields to other SerializedTypes (even an STObject +itself). An STArray is an array of STObjects with a single Field, mapped to an +STObject, containing an arbitrary amount of Field -> SerializedType pairs. (See +notes below on differences to C++ implementation) + +## Types + +Following is a survey of some of the classes, sorted somewhat topologically. + +The following json representation of metadata related to a finalized transaction +can be used as a concrete example and will be referenced later. + +```java +/** + * + * # Transaction Meta JSON + * + * { + * "TransactionResult": "tesSUCCESS", + * + * {@link com.ripple.core.types.known.tx.result.TransactionResult} + * {@link com.ripple.core.fields.Field.TransactionResult} + * {@link com.ripple.core.coretypes.uint.UInt8} + * + * "TransactionIndex": 0, + * + * {@link com.ripple.core.types.known.tx.result.TransactionMeta#transactionIndex} + * {@link com.ripple.core.coretypes.uint.UInt32} + * + * "AffectedNodes": [ + * + * {@link com.ripple.core.types.known.tx.result.TransactionMeta#affectedNodes} + * + * { + + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode} + + * + * "NewFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()} + * + * "Sequence": 103929, + * + * {@link com.ripple.core.fields.Field#Sequence} + * + * + * "TakerGets": { + * + * {@link com.ripple.core.coretypes.Amount} + * + * "currency": "ILS", + * + * {@link com.ripple.core.coretypes.Currency} + * + * "value": "1694.768", + * + * {@link com.ripple.core.coretypes.AccountID} + * + * "issuer": "rNPRNzBB92BVpAhhZr4iXDTveCgV5Pofm9" + * + * {@link java.math.BigDecimal} + * + * }, + * + * + * "Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3", + * + * {@link com.ripple.core.coretypes.AccountID} + * + * "BookDirectory": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000", + * + * {@link com.ripple.core.coretypes.Quality#fromBookDirectory} + * + * "TakerPays": "98957503520", + * + * {@link com.ripple.core.coretypes.Amount} + * {@link com.ripple.core.coretypes.Amount#isNative()} + * + * "OwnerNode": "000000000000000E" + * + * {@link com.ripple.core.types.known.sle.entries.Offer#ownerNodeDirectoryIndex} + * }, + * + * "LedgerIndex": "3596CE72C902BAFAAB56CC486ACAF9B4AFC67CF7CADBB81A4AA9CBDC8C5CB1AA", + * + * {@link com.ripple.core.coretypes.hash.Hash256#index} + * + * "LedgerEntryType": "Offer" + * + * {@link com.ripple.core.serialized.enums.LedgerEntryType} + * {@link com.ripple.core.types.known.sle.entries.Offer} + * } + * }, + * + * + * { + + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode} + + * + * "NewFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()} + * + * "RootIndex": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000", + * + * com.ripple.core.coretypes.hash.Hash256#index + * + * "TakerGetsIssuer": "92D705968936C419CE614BF264B5EEB1CEA47FF4", + * + * {@link com.ripple.core.coretypes.Currency} + * {@link com.ripple.core.coretypes.hash.Hash160} + * {@link com.ripple.core.fields.Field#TakerGetsIssuer} + * + * "ExchangeRate": "5C14BE8A20D7F000", + * + * {@link com.ripple.core.coretypes.Quality#fromBookDirectory} + * + * "TakerGetsCurrency": "000000000000000000000000494C530000000000" + * + * {@link com.ripple.core.coretypes.Currency} + * {@link com.ripple.core.coretypes.hash.Hash160} + * {@link com.ripple.core.fields.Field#TakerGetsCurrency} + * + * }, + * + * "LedgerIndex": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000", + * + * {@link com.ripple.core.coretypes.hash.Hash256#index} + * + * "LedgerEntryType": "DirectoryNode" + * + * {@link com.ripple.core.types.known.sle.entries.DirectoryNode} + * {@link com.ripple.core.serialized.enums.LedgerEntryType#DirectoryNode} + * } + * }, + * + * { + * "ModifiedNode": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode} + * {@link com.ripple.core.types.known.tx.result.AffectedNode#isModifiedNode()} + * + * "FinalFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()} + * + * "RootIndex": "801C5AFB5862D4666D0DF8E5BE1385DC9B421ED09A4269542A07BC0267584B64", + * + * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#rootIndex} + * + * "Flags": 0, + * "Owner": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3", + * + * {@link com.ripple.core.coretypes.hash.Index#ownerDirectory} + * + * "IndexPrevious": "0000000000000000" + * + * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#hasPreviousIndex} + * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#prevIndex} + * }, + * + * "LedgerIndex": "AB03F8AA02FFA4635E7CE2850416AEC5542910A2B4DBE93C318FEB08375E0DB5", + * + * "LedgerEntryType": "DirectoryNode" + * } + * }, + * { + * "ModifiedNode": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode} + * {@link com.ripple.core.types.known.tx.result.AffectedNode#isModifiedNode()} + * + * "FinalFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()} + * + * "Sequence": 103930, + * "Flags": 0, + * "Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3", + * "OwnerCount": 9, + * "Balance": "106861218302" + * + * {@link com.ripple.core.coretypes.Amount} + * }, + * + * "LedgerIndex": "CF23A37E39A571A0F22EC3E97EB0169936B520C3088963F16C5EE4AC59130B1B", + * + * {@link com.ripple.core.coretypes.hash.Index#accountRoot} + * + * "LedgerEntryType": "AccountRoot", + * + * {@link com.ripple.core.types.known.sle.entries.AccountRoot} + * {@link com.ripple.core.serialized.enums.LedgerEntryType#AccountRoot} + * + * "PreviousFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsPrevious()} + * + * "Sequence": 103929, + * "OwnerCount": 8, + * "Balance": "106861218312" + * }, + * + * "PreviousTxnID": "DE15F43F4A73C4F6CB1C334D9E47BDE84467C0902796BB81D4924885D1C11E6D", + * + * {@link com.ripple.core.types.known.tx.Transaction#hash} + * + * "PreviousTxnLgrSeq": 3225338 + * + * {@link com.ripple.core.coretypes.uint.UInt32} + * } + * } + * ] + * } +*/ +``` + +* Note that ALL field names are, by convention, upper case (in fact field names (index, hash) may be +lower cased but are not serialized) + +* The `AffectedNodes` is an STArray. As stated, the immediate +children each contain only a single key (or [Field](src/main/java/com/ripple/core/fields/Field.java#L138-L140)) + + * ModifiedNode + +Moving on. + +``` +com +└── ripple + ├── serialized + ├── core + │ + ├── enums + │   ├── LedgerEntryType + │   ├── EngineResult + │   └── TransactionType +``` + +* In the json above look at the [TransactionResult](src/main/java/com/ripple/core/fields/Field.java#L164) field. + Note that it has a Type of of UINT8, yet clearly it's represented in json as a string. + +#### com.ripple.core.fields.Type + + This is a simple Java enum(eration) of the various types. eg. + + UINT32(2) + + This definition implies giving the static ordinal `2` to the UINT32 type. + +#### com.ripple.core.fields.Field + + Consider the following definition of a field + + QualityIn(20, Type.UINT32) + + As stated before a Field has name and type ordinals, but it also has an + implied symbolic string representation (as seen used in the json above) + + The string name can be looked up via a `code`, which is an integer created by + shifting the type ordinal 16 bits to the left and ORing it with the name. + + See: [com.ripple.core.fields.Field#fromCode](src/main/java/com/ripple/core/fields/Field.java) + +#### com.ripple.core.fields.HasField + + This is simply an interface for returning a Field. We know that a Field + implies a Type and a name and there's a set amount of them. For each concrete + class implementation of a given Type, we create a XXXfield class that + implements HasField + + eg. + + ```java + protected abstract static class STArrayField implements HasField{} + public static STArrayField starrayField(final Field f) { + return new STArrayField(){ @Override public Field getField() {return f;}}; + } + ``` + + Then we can create static members on the concrete class + + ```java + static public STArrayField AffectedNodes = starrayField(Field.AffectedNodes); + static public STArrayField Signatures = starrayField(Field.Signatures); + static public STArrayField Template = starrayField(Field.Template); + ``` + + Later this is used create an api that looks as so + + ```java + if (transactionType() == TransactionType.Payment && meta.has(STArray.AffectedNodes)) { + STArray affected = meta.get(STArray.AffectedNodes); + for (STObject node : affected) { + if (node.has(STObject.CreatedNode)) { + STObject created = node.get(STObject.CreatedNode); + ``` + + This is implemented by overloading get() + + ```java + public STArray get(STArray.STArrayField f) { + return (STArray) fields.get(f.getField()); + } + ``` + +### com.ripple.core.serialized + +#### com.ripple.core.serialized.SerializedType + +```java +public interface SerializedType { + Object toJSON(); + byte[] toBytes(); + String toHex(); + void toBytesSink(BytesSink to); + Type type(); +} +``` + +#### com.ripple.core.serialized.BytesList + +A dynamic array of byte[]. Used by TypeTranslators to avoid needless +copying (see fromParser(parser, hint)). + +#### com.ripple.core.serialized.BinaryParser + +Responsible for decoding Fields and VL encoded structures. + +#### com.ripple.core.serialized.TypeTranslator + +Handles converting a SerializedType instances to/from json, binary and other non +SerializedType values. + +Has methods like fromHex, fromBytes, which delegate to fromParser. + +#### com.ripple.core.serialized.BinarySerializer + +Responsible for encoding Fields/SerializeType into binary. + +## Notes + +* In the C++ implementation of serialized objects, an STObject can, itself, be + assigned a Field, which is stored outside of the associative structure. + + This is problematic when storing as json. Consider this pseudocode. + + ```python + >>> so = STObject() + >>> so.name = "FieldName" + >>> so["FieldOfDreams"] = "A Kevin Costner Movie" + >>> sa = STArray([so]) + ``` + + How could `sa` be declared as json? + + ```json + >>> [{"FieldName" : {"FieldOfDreams": "A Kevin Costner Movie"}}] + ``` + + This is in fact how rippled works. There is no 1:1 mapping of STObject to {} + + In ripple-lib(-java)? there is, and single key children of STArrays are enforced. + +* Ripple uses 32 byte hashes for object indexes, taking half of a SHA512 hash, + which is ~%33 faster than SHA256. + +* Simply using google protocol buffers was considered inadequate [link](https://github.com/ripple/rippled/blob/ee51968820fc41c5aeadf2067bfdae54ff21fa66/BinaryFormats.txt#L16) \ No newline at end of file diff --git a/ripple-core/build.gradle b/ripple-core/build.gradle new file mode 100644 index 0000000000..7ee498ed2c --- /dev/null +++ b/ripple-core/build.gradle @@ -0,0 +1,52 @@ +apply plugin: 'java' +apply plugin: 'kotlin' + +description = 'ripple-core' + +version = '0.0.1-SNAPSHOT' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + compile 'net.i2p.crypto:eddsa:0.2.0' + compile 'org.bouncycastle:bcprov-jdk15on:1.58' + compile 'org.json:json:20171018' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.3' + testCompile 'junit:junit:4.12' + testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + testCompile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5" +} +buildscript { + ext.kotlin_version = '1.3.0' + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} +repositories { + mavenCentral() +} +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +task getDeps(type: Copy) { + from configurations.compile + into "$rootDir/jars/" +} + +kotlin { + experimental { + coroutines "enable" + } +} \ No newline at end of file diff --git a/ripple-core/src/main/java/README.java b/ripple-core/src/main/java/README.java new file mode 100644 index 0000000000..0d01f2dfc7 --- /dev/null +++ b/ripple-core/src/main/java/README.java @@ -0,0 +1,206 @@ +/** + * + * # Transaction Meta JSON + * + * { + * "TransactionResult": "tesSUCCESS", + * + * {@link com.ripple.core.types.known.tx.result.TransactionResult} + * {@link com.ripple.core.fields.Field.TransactionResult} + * {@link com.ripple.core.coretypes.uint.UInt8} + * + * "TransactionIndex": 0, + * + * {@link com.ripple.core.types.known.tx.result.TransactionMeta#transactionIndex} + * {@link com.ripple.core.coretypes.uint.UInt32} + * + * "AffectedNodes": [ + * + * {@link com.ripple.core.types.known.tx.result.TransactionMeta#affectedNodes} + * + * { + * "CreatedNode": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode} + * {@link com.ripple.core.types.known.tx.result.AffectedNode#isCreatedNode()} + * + * "NewFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()} + * + * "Sequence": 103929, + * + * {@link com.ripple.core.fields.Field#Sequence} + * + * + * "TakerGets": { + * + * {@link com.ripple.core.coretypes.Amount} + * + * "currency": "ILS", + * + * {@link com.ripple.core.coretypes.Currency} + * + * "value": "1694.768", + * + * {@link com.ripple.core.coretypes.AccountID} + * + * "issuer": "rNPRNzBB92BVpAhhZr4iXDTveCgV5Pofm9" + * + * {@link java.math.BigDecimal} + * + * }, + * + * + * "Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3", + * + * {@link com.ripple.core.coretypes.AccountID} + * + * "BookDirectory": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000", + * + * {@link com.ripple.core.coretypes.Quality#fromBookDirectory} + * + * "TakerPays": "98957503520", + * + * {@link com.ripple.core.coretypes.Amount} + * {@link com.ripple.core.coretypes.Amount#isNative()} + * + * "OwnerNode": "000000000000000E" + * + * {@link com.ripple.core.types.known.sle.entries.Offer#ownerNodeDirectoryIndex} + * }, + * + * "LedgerIndex": "3596CE72C902BAFAAB56CC486ACAF9B4AFC67CF7CADBB81A4AA9CBDC8C5CB1AA", + * + * {@link com.ripple.core.coretypes.hash.Hash256#index} + * + * "LedgerEntryType": "Offer" + * + * {@link com.ripple.core.serialized.enums.LedgerEntryType} + * {@link com.ripple.core.types.known.sle.entries.Offer} + * } + * }, + * + * + * { + * "CreatedNode": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode} + * {@link com.ripple.core.types.known.tx.result.AffectedNode#isCreatedNode()} + * + * "NewFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()} + * + * "RootIndex": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000", + * + * com.ripple.core.coretypes.hash.Hash256#index + * + * "TakerGetsIssuer": "92D705968936C419CE614BF264B5EEB1CEA47FF4", + * + * {@link com.ripple.core.coretypes.Currency} + * {@link com.ripple.core.coretypes.hash.Hash160} + * {@link com.ripple.core.fields.Field#TakerGetsIssuer} + * + * "ExchangeRate": "5C14BE8A20D7F000", + * + * {@link com.ripple.core.coretypes.Quality#fromBookDirectory} + * + * "TakerGetsCurrency": "000000000000000000000000494C530000000000" + * + * {@link com.ripple.core.coretypes.Currency} + * {@link com.ripple.core.coretypes.hash.Hash160} + * {@link com.ripple.core.fields.Field#TakerGetsCurrency} + * + * }, + * + * "LedgerIndex": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000", + * + * {@link com.ripple.core.coretypes.hash.Hash256#index} + * + * "LedgerEntryType": "DirectoryNode" + * + * {@link com.ripple.core.types.known.sle.entries.DirectoryNode} + * {@link com.ripple.core.serialized.enums.LedgerEntryType#DirectoryNode} + * } + * }, + * + * { + * "ModifiedNode": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode} + * {@link com.ripple.core.types.known.tx.result.AffectedNode#isModifiedNode()} + * + * "FinalFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()} + * + * "RootIndex": "801C5AFB5862D4666D0DF8E5BE1385DC9B421ED09A4269542A07BC0267584B64", + * + * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#rootIndex} + * + * "Flags": 0, + * "Owner": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3", + * + * {@link com.ripple.core.coretypes.hash.Index#ownerDirectory} + * + * "IndexPrevious": "0000000000000000" + * + * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#hasPreviousIndex} + * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#prevIndex} + * }, + * + * "LedgerIndex": "AB03F8AA02FFA4635E7CE2850416AEC5542910A2B4DBE93C318FEB08375E0DB5", + * + * "LedgerEntryType": "DirectoryNode" + * } + * }, + * { + * "ModifiedNode": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode} + * {@link com.ripple.core.types.known.tx.result.AffectedNode#isModifiedNode()} + * + * "FinalFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()} + * + * "Sequence": 103930, + * "Flags": 0, + * "Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3", + * "OwnerCount": 9, + * "Balance": "106861218302" + * + * {@link com.ripple.core.coretypes.Amount} + * }, + * + * "LedgerIndex": "CF23A37E39A571A0F22EC3E97EB0169936B520C3088963F16C5EE4AC59130B1B", + * + * {@link com.ripple.core.coretypes.hash.Index#accountRoot} + * + * "LedgerEntryType": "AccountRoot", + * + * {@link com.ripple.core.types.known.sle.entries.AccountRoot} + * {@link com.ripple.core.serialized.enums.LedgerEntryType#AccountRoot} + * + * "PreviousFields": { + * + * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsPrevious()} + * + * "Sequence": 103929, + * "OwnerCount": 8, + * "Balance": "106861218312" + * }, + * + * "PreviousTxnID": "DE15F43F4A73C4F6CB1C334D9E47BDE84467C0902796BB81D4924885D1C11E6D", + * + * {@link com.ripple.core.types.known.tx.Transaction#hash} + * + * "PreviousTxnLgrSeq": 3225338 + * + * {@link com.ripple.core.coretypes.uint.UInt32} + * } + * } + * ] + * } +*/ \ No newline at end of file diff --git a/ripple-core/src/main/java/com/ripple/core/binary/FileSTWriter.java b/ripple-core/src/main/java/com/ripple/core/binary/FileSTWriter.java new file mode 100644 index 0000000000..e46af9db43 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/binary/FileSTWriter.java @@ -0,0 +1,32 @@ +package com.ripple.core.binary; + +import com.ripple.core.serialized.StreamSink; + +import java.io.*; + +public class FileSTWriter extends STWriter implements Closeable { + BufferedOutputStream out; + + private FileSTWriter(StreamSink sink, BufferedOutputStream out) { + super(sink); + this.out = out; + } + + public static FileSTWriter fromFile(String path) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(path); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + BufferedOutputStream out = new BufferedOutputStream(fos); + StreamSink sink = new StreamSink(out); + + return new FileSTWriter(sink, out); + } + + @Override + public void close() throws IOException { + out.close(); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/binary/STReader.java b/ripple-core/src/main/java/com/ripple/core/binary/STReader.java new file mode 100644 index 0000000000..dc3655073b --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/binary/STReader.java @@ -0,0 +1,126 @@ +package com.ripple.core.binary; + +import com.ripple.core.coretypes.*; +import com.ripple.core.coretypes.hash.Hash128; +import com.ripple.core.coretypes.hash.Hash160; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.uint.UInt16; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.coretypes.uint.UInt8; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.StreamBinaryParser; +import com.ripple.core.types.known.sle.LedgerEntry; +import com.ripple.core.types.known.tx.Transaction; +import com.ripple.core.types.known.tx.result.TransactionMeta; +import com.ripple.core.types.known.tx.result.TransactionResult; + +import java.util.Arrays; +import java.util.Date; + +public class STReader { + protected BinaryParser parser; + public STReader(BinaryParser parser) { + this.parser = parser; + } + public STReader(String hex) { + this.parser = new BinaryParser(hex); + } + + public static STReader fromFile(String arg) { + return new STReader(StreamBinaryParser.fromFile(arg)); + } + + public UInt8 uInt8() { + return UInt8.fromParser(parser); + } + public UInt16 uInt16() { + return UInt16.fromParser(parser); + } + public UInt32 uInt32() { + return UInt32.fromParser(parser); + } + public UInt64 uInt64() { + return UInt64.fromParser(parser); + } + public Hash128 hash128() { + return Hash128.fromParser(parser); + } + public Hash160 hash160() { + return Hash160.fromParser(parser); + } + public Currency currency() { + return Currency.fromParser(parser); + } + public Hash256 hash256() { + return Hash256.fromParser(parser); + } + public Vector256 vector256() { + return Vector256.fromParser(parser); + } + public AccountID accountID() { + return AccountID.fromParser(parser); + } + public Blob variableLength() { + int hint = parser.readVLLength(); + return Blob.fromParser(parser, hint); + } + public Amount amount() { + return Amount.fromParser(parser); + } + public PathSet pathSet() { + return PathSet.fromParser(parser); + } + + public STObject stObject() { + return STObject.fromParser(parser); + } + public STObject vlStObject() { + return STObject.fromParser(parser, parser.readVLLength()); + } + + public HashPrefix hashPrefix() { + byte[] read = parser.read(4); + for (HashPrefix hashPrefix : HashPrefix.values()) { + if (Arrays.equals(read, hashPrefix.bytes())) { + return hashPrefix; + } + } + return null; + } + + public STArray stArray() { + return STArray.fromParser(parser); + } + public Date rippleDate() { + return RippleDate.fromParser(parser); + } + + public BinaryParser parser() { + return parser; + } + + public TransactionResult readTransactionResult(UInt32 ledgerIndex) { + Hash256 hash = hash256(); + Transaction txn = (Transaction) vlStObject(); + TransactionMeta meta = (TransactionMeta) vlStObject(); + return new TransactionResult(ledgerIndex.longValue(), hash, txn, meta); + } + + public LedgerEntry readLE() { + Hash256 index = hash256(); + STObject object = vlStObject(); + LedgerEntry le = (LedgerEntry) object; + le.index(index); + return le; + } + + public int readOneInt() { + return parser.readOneInt(); + } + + public boolean end() { + return parser.end(); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/binary/STWriter.java b/ripple-core/src/main/java/com/ripple/core/binary/STWriter.java new file mode 100644 index 0000000000..77f28eb7f7 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/binary/STWriter.java @@ -0,0 +1,71 @@ +package com.ripple.core.binary; + +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.serialized.BinarySerializer; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.StreamSink; +import com.ripple.core.types.known.sle.LedgerEntry; +import com.ripple.core.types.known.tx.result.TransactionResult; + +import java.io.*; + +public class STWriter implements BytesSink, Closeable { + BytesSink sink; + BinarySerializer serializer; + public STWriter(BytesSink bytesSink) { + serializer = new BinarySerializer(bytesSink); + sink = bytesSink; + } + + private OutputStream stream; + private STWriter(BytesSink sink, OutputStream stream) { + this(sink); + this.stream = stream; + } + + public static STWriter toFile(String path) { + try { + FileOutputStream fos = new FileOutputStream(path); + BufferedOutputStream bos = new BufferedOutputStream(fos); + return new STWriter(new StreamSink(bos), bos); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + public void write(SerializedType obj) { + obj.toBytesSink(sink); + } + public void writeVl(SerializedType obj) { + serializer.addLengthEncoded(obj); + } + + @Override + public void add(byte aByte) { + sink.add(aByte); + } + + @Override + public void add(byte[] bytes) { + sink.add(bytes); + } + + public void write(TransactionResult result) { + write(result.hash); + writeVl(result.txn); + writeVl(result.meta); + } + + public void write(Hash256 hash256, LedgerEntry le) { + write(hash256); + writeVl(le); + } + + @Override + public void close() throws IOException { + if (this.stream != null) { + this.stream.close(); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/cache/SLECache.java b/ripple-core/src/main/java/com/ripple/core/cache/SLECache.java new file mode 100644 index 0000000000..55552ede01 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/cache/SLECache.java @@ -0,0 +1,110 @@ +package com.ripple.core.cache; + +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.types.known.sle.LedgerEntry; +import com.ripple.core.types.known.tx.result.AffectedNode; +import com.ripple.core.types.known.tx.result.TransactionMeta; +import com.ripple.core.types.known.tx.result.TransactionResult; + +import java.util.TreeMap; + +public class SLECache { + private final TreeMap cache = new TreeMap(); + + public static class CacheEntry { + public LedgerEntry le; + public UInt32 prevTxnIndex; + public UInt32 prevLedger; + public boolean deleted = false; + + public void upateLedgerEntry(LedgerEntry le, UInt32 ledgerIndex, UInt32 txnIndex) { + if (doUpdate(txnIndex, ledgerIndex)) { + // in the first case + prevTxnIndex = txnIndex; + prevLedger = ledgerIndex; + + if (le == null) { + deleted = true; + } + this.le = le; + } + } + + private boolean doUpdate(UInt32 txnIndex, UInt32 ledgerIndex) { + if (le == null && !deleted) { + return true; + } + if (prevLedger == null) { + return true; + } + int ledgerCmp = ledgerIndex.compareTo(prevLedger); + if (ledgerCmp == 1) { + return true; + } + if (ledgerCmp == 0) { + if (prevTxnIndex == null) { + // We don't know, should log a warning or something + // Should we keep the first one that we have of this index + // or can we assume that the latest is the best? + return true; + } + if (txnIndex.compareTo(prevTxnIndex) == 1) { + // This happened AFTER + return true; + } + } + //ledgerCmp == -1 or txnIndex <= previousTxnIndex ss + return false; + } + } + + public boolean cache(LedgerEntry le, UInt32 validatedLedgerIndex) { + Hash256 index = le.ledgerIndex(); + CacheEntry ce = getOrCreate(index); + ce.upateLedgerEntry(le, validatedLedgerIndex, null); + return true; + } + + private CacheEntry getEntry(Hash256 index) { + return cache.get(index); + } + + private CacheEntry createEntry(Hash256 index) { + CacheEntry ce = new CacheEntry(); + cache.put(index, ce); + return ce; + } + + public LedgerEntry get(Hash256 index) { + CacheEntry entry = getEntry(index); + return entry == null || entry.deleted ? null : entry.le; + } + + public void updateFromTransactionResult(TransactionResult tr) { + if (!tr.validated) { + return; + } + + TransactionMeta meta = tr.meta; + UInt32 ledgerIndex = tr.ledgerIndex; + UInt32 txnIndex = meta.transactionIndex(); + + for (AffectedNode an : meta.affectedNodes()) { + Hash256 index = an.ledgerIndex(); + CacheEntry ce = getOrCreate(index); + ce.upateLedgerEntry(an.isDeletedNode() ? null : (LedgerEntry) an.nodeAsFinal(), + ledgerIndex, + txnIndex); + } + } + + private CacheEntry getOrCreate(Hash256 index) { + CacheEntry already = getEntry(index); + if (already == null) { + return createEntry(index); + } else { + return already; + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/AccountID.java b/ripple-core/src/main/java/com/ripple/core/coretypes/AccountID.java new file mode 100644 index 0000000000..606c5b2b71 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/AccountID.java @@ -0,0 +1,188 @@ +package com.ripple.core.coretypes; + +import com.ripple.core.coretypes.hash.Hash160; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.Index; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.fields.AccountIDField; +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.TypeTranslator; +import com.ripple.crypto.Seed; +import com.ripple.crypto.keys.IKeyPair; +import com.ripple.encodings.addresses.Addresses; +import com.ripple.encodings.common.B16; +import com.ripple.utils.Utils; + +/** + * Originally it was intended that AccountIDs would be variable length so that's + * why they are variable length encoded as top level field objects. + * + * Note however, that in practice, all account ids are just 160 bit hashes. + * Consider the fields TakerPaysIssuer and fixed length encoding of issuers in + * amount serializations. + * + * Thus, we extend Hash160 which affords us some functionality. + */ +public class AccountID extends Hash160 { + public static final AccountID NEUTRAL = fromInteger(1); + public static final AccountID XRP_ISSUER = fromInteger(0); + + public final String address; + + public AccountID(byte[] bytes) { + this(bytes, encodeAddress(bytes)); + } + + public AccountID(byte[] bytes, String address) { + super(bytes); + this.address = address; + } + + // Static from* constructors + public static AccountID fromString(String value) { + if (value.length() == 160 / 4) { + return fromBytes(B16.decode(value)); + } else { + return fromAddress(value); + } + } + + static public AccountID fromAddress(String address) { + byte[] bytes = Addresses.decodeAccountID(address); + return new AccountID(bytes, address); + } + + static public AccountID fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + + static public AccountID fromHex(String hex) { + return translate.fromHex(hex); + } + + public static AccountID fromKeyPair(IKeyPair kp) { + byte[] bytes = kp.id(); + return new AccountID(bytes, encodeAddress(bytes)); + } + + public static AccountID fromPassPhrase(String phrase) { + return fromKeyPair(Seed.fromPassPhrase(phrase).keyPair()); + } + + static public AccountID fromSeed(String seed) { + return fromKeyPair(Seed.getKeyPair(seed)); + } + + private static AccountID fromInteger(Integer n) { + return fromBytes(Utils.padTo160(new UInt32(n).toByteArray())); + } + + public static AccountID fromBytes(byte[] bytes) { + return new AccountID(bytes, encodeAddress(bytes)); + } + + @Override + public int hashCode() { + return address.hashCode(); + } + + @Override + public String toString() { + return address; + } + + public Issue issue(String code) { + return new Issue(Currency.fromString(code), this); + } + + public Issue issue(Currency c) { + return new Issue(c, this); + } + + public boolean isNativeIssuer() { + return this == XRP_ISSUER || equals(XRP_ISSUER); + } + + // SerializedType interface implementation + @Override + public Object toJSON() { + return toString(); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + to.add(bytes()); + } + + @Override + public Type type() { + return Type.AccountID; + } + + public Hash256 lineIndex(Issue issue) { + if (issue.isNative()) throw new AssertionError(); + return Index.rippleState(this, issue.issuer(), issue.currency()); + } + + public static class Translator extends TypeTranslator { + @Override + public AccountID fromParser(BinaryParser parser, Integer hint) { + if (hint == null) { + hint = 20; + } + return AccountID.fromBytes(parser.read(hint)); + } + + @Override + public String toString(AccountID obj) { + return obj.toString(); + } + + @Override + public AccountID fromString(String value) { + return AccountID.fromString(value); + } + } + + // + + static public Translator translate = new Translator(); + + // helpers + + private static String encodeAddress(byte[] address) { + return Addresses.encodeAccountID(address); + } + + // Typed field definitions + private static AccountIDField accountField(final Field f) { + return new AccountIDField() { + @Override + public Field getField() { + return f; + } + }; + } + + static public AccountIDField Account = accountField(Field.Account); + static public AccountIDField Owner = accountField(Field.Owner); + static public AccountIDField Destination = accountField(Field.Destination); + static public AccountIDField Issuer = accountField(Field.Issuer); + static public AccountIDField Target = accountField(Field.Target); + static public AccountIDField RegularKey = accountField(Field.RegularKey); + static public AccountIDField Authorize = accountField(Field.Authorize); + static public AccountIDField Unauthorize = accountField(Field.Unauthorize); +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Amount.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Amount.java new file mode 100644 index 0000000000..3813a5ac09 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Amount.java @@ -0,0 +1,790 @@ +package com.ripple.core.coretypes; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.fields.AmountField; +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import org.json.JSONObject; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.Objects; + +/** + * In ripple, amounts are either XRP, the native currency, or an IOU of + * a given currency as issued by a designated account. + */ +public class Amount extends Number implements SerializedType, Comparable + +{ + + private static BigDecimal TAKER_PAYS_FOR_THAT_DAMN_OFFER = new BigDecimal("1000000000000.000100"); + +// public static final Amount NEUTRAL_ZERO = new Amount(Currency.NEUTRAL, AccountID.NEUTRAL); + + /** + * Thrown when an Amount is constructed with an invalid value + */ + public static class PrecisionError extends RuntimeException { + public Amount illegal; + + public PrecisionError(String s) { + super(s); + } + + public PrecisionError(String s, Amount amount) { + super(s); + illegal = amount; + } + } + + // For rounding/multiplying/dividing + public static final MathContext MATH_CONTEXT = new MathContext(16, RoundingMode.HALF_UP); + // The maximum amount of digits in mantissa of an IOU amount + public static final int MAXIMUM_IOU_PRECISION = 16; + // The smallest quantity of an XRP is a drop, 1 millionth of an XRP + public static final int MAXIMUM_NATIVE_SCALE = 6; + // Defines bounds for native amounts + public static final BigDecimal MAX_NATIVE_VALUE = parseDecimal("100,000,000,000.0"); + public static final BigDecimal MIN_NATIVE_VALUE = parseDecimal("0.000,001"); + + // These are flags used when serializing to binary form + public static final UInt64 BINARY_FLAG_IS_IOU = new UInt64("8000000000000000", 16); + public static final UInt64 BINARY_FLAG_IS_NON_NEGATIVE_NATIVE = new UInt64("4000000000000000", 16); + + public static final Amount ONE_XRP = fromString("1.0"); + + // TODO, even though all these fields are effectively final, and only + // ever set it would be nice to actually use the final modifier. + // Perhaps create an amount builder. + + // The quantity of XRP or Issue(currency/issuer pairing) + // When native, the value unit is XRP, not drops. + private BigDecimal value; + private Currency currency; + // If the currency is XRP + private boolean isNative; + // Normally, in the constructor of an Amount the value is checked + // that it's scale/precision and quantity are correctly bounded. + // If unbounded is true, these checks are skipped. + // This is there for historical ledgers that contain amounts that + // would now be considered malformed (in the sense of the transaction + // engine result class temMALFORMED) + private boolean unbounded = false; + // The ZERO account is used for specifying the issuer for native + // amounts. In practice the issuer is never used when an + // amount is native. + private AccountID issuer; + + private UInt64 mantissa; + // The exponent is always calculated. + private int exponent; + + public Amount(BigDecimal value, Currency currency, AccountID issuer) { + this(value, currency, issuer, false); + } + + public Amount(BigDecimal xrpScaleValue) { + isNative = true; + currency = Currency.XRP; + this.setAndCheckValue(xrpScaleValue); + } + + public Amount(Number xrpScaleValue) { + isNative = true; + currency = Currency.XRP; + this.setAndCheckValue(BigDecimal.valueOf(xrpScaleValue.doubleValue())); + } + + public Amount(BigDecimal value, Currency currency, AccountID issuer, boolean isNative, boolean unbounded) { + this.isNative = isNative; + this.currency = currency; + this.unbounded = unbounded; + this.setAndCheckValue(value); + // done AFTER set value which sets some default values + this.issuer = issuer; + } + + public Amount(Currency currency, AccountID account) { + this(BigDecimal.ZERO, currency, account); + } + + // Private constructors + Amount(BigDecimal newValue, Currency currency, AccountID issuer, boolean isNative) { + this(newValue, currency, issuer, isNative, false); + } + + private Amount(BigDecimal value, String currency, String issuer) { + this(value, currency); + if (issuer != null) { + this.issuer = AccountID.fromString(issuer); + } + } + + public Amount(BigDecimal value, String currency) { + isNative = false; + this.currency = Currency.fromString(currency); + this.setAndCheckValue(value); + } + + private void setAndCheckValue(BigDecimal value) { + this.value = value.stripTrailingZeros(); + initialize(); + } + + private void initialize() { + if (isNative()) { + issuer = AccountID.XRP_ISSUER; + if (!unbounded) { + checkXRPBounds(); + } + // Offset is unused for native amounts + exponent = -6; // compared to drops. + } else { + issuer = AccountID.NEUTRAL; + exponent = calculateExponent(); + + if (value.precision() > MAXIMUM_IOU_PRECISION && !unbounded) { + String err = "value precision of " + value.precision() + + " is greater than maximum " + + "iou precision of " + MAXIMUM_IOU_PRECISION; + throw new PrecisionError(err, this); + } + } + mantissa = calculateMantissa(); + } + + private Amount newValue(BigDecimal newValue) { + return newValue(newValue, false, false); + } + + private Amount newValue(BigDecimal newValue, boolean round, boolean unbounded) { + if (round) { + newValue = roundValue(newValue, isNative); + } + return new Amount(newValue, currency, issuer, isNative, unbounded); + } + + private Amount newValue(BigDecimal val, boolean round) { + return newValue(val, round, false); + } + + /* Getters and Setters */ + + public BigDecimal value() { + return value; + } + + public Currency currency() { + return currency; + } + + public AccountID issuer() { + return issuer; + } + + public Issue issue() { + // TODO: store the currency and issuer as an Issue + return new Issue(currency, issuer); + } + + private UInt64 mantissa() { + // Having this lazily computed and then cached would give significant + // performance boosts in some single threaded contexts. Given the inputs + // for the method are at least effectively final, and the method is + // essentially a pure function, it seems probably safe to do. It + // seems at worst, that in a multi threaded context, some extra work + // will be done in rare cases? + // TODO: check this assumption + return mantissa; + } + + public int exponent() { + return exponent; + } + + public boolean isNative() { + return isNative; + } + + public String currencyString() { + return currency.toString(); + } + + public String issuerString() { + if (issuer == null) { + return ""; + } + return issuer.toString(); + } + + /* Offset & Mantissa Helpers */ + + /** + * @return a positive value for the mantissa + */ + private UInt64 calculateMantissa() { + if (isNative()) { + return new UInt64(bigIntegerDrops().abs()); + } else { + return new UInt64(bigIntegerIOUMantissa()); + } + } + + private int calculateExponent() { + return -MAXIMUM_IOU_PRECISION + value.precision() - value.scale(); + } + + private BigInteger bigIntegerIOUMantissa() { + return exactBigIntegerScaledByPowerOfTen(-exponent).abs(); + } + + private BigInteger bigIntegerDrops() { + return exactBigIntegerScaledByPowerOfTen(MAXIMUM_NATIVE_SCALE); + } + + private BigInteger exactBigIntegerScaledByPowerOfTen(int n) { + return value.scaleByPowerOfTen(n).toBigIntegerExact(); + } + + /* Equality testing */ + + private boolean equalValue(Amount amt) { + return compareTo(amt) == 0; + } + @Override + public boolean equals(Object obj) { + if (obj instanceof Amount) { + return equals((Amount) obj); + } + return super.equals(obj); + } + + public boolean equals(Amount amt) { + return equalValue(amt) && + currency.equals(amt.currency) && + (isNative() || issuer.equals(amt.issuer)); + } + + public boolean equalsExceptIssuer(Amount amt) { + return equalValue(amt) && + currencyString().equals(amt.currencyString()); + } + + public int compareTo(Amount amount) { + Objects.requireNonNull(amount); + return value.compareTo(amount.value); + } + + public boolean isZero() { + return value.signum() == 0; + } + + public boolean isNegative() { + return value.signum() == -1; + } + + // Maybe you want !isNegative() + // Any amount that !isNegative() isn't necessarily positive + // Is a zero amount strictly positive? no + public boolean isPositive() { + return value.signum() == 1; + } + + /** + + Arithmetic Operations + + There's no checking if an amount is of a different currency/issuer. + + All operations return amounts of the same currency/issuer as the + first operand. + + eg. + + amountOne.add(amountTwo) + + The currency/issuer of the resultant amount, is that of `amountOne` + + Divide and multiply are equivalent to the javascript ripple-lib + ratio_human and product_human. + + */ + public Amount add(BigDecimal augend) { + return newValue(value.add(augend), true); + } + + public Amount add(Amount augend) { + return add(augend.value); + } + + public Amount add(Number augend) { + return add(BigDecimal.valueOf(augend.doubleValue())); + } + + public Amount subtract(BigDecimal subtrahend) { + return newValue(value.subtract(subtrahend), true); + } + + public Amount subtract(Amount subtrahend) { + return subtract(subtrahend.value); + } + + public Amount subtract(Number subtrahend) { + return subtract(BigDecimal.valueOf(subtrahend.doubleValue())); + } + + public Amount multiply(BigDecimal divisor) { + return newValue(value.multiply(divisor, MATH_CONTEXT), true); + } + + public Amount multiply(Amount multiplicand) { + return multiply(multiplicand.value); + } + + public Amount multiply(Number multiplicand) { + return multiply(BigDecimal.valueOf(multiplicand.doubleValue())); + } + + public Amount divide(BigDecimal divisor) { + return newValue(value.divide(divisor, MATH_CONTEXT), true); + } + + public Amount divide(Amount divisor) { + return divide(divisor.value); + } + + public Amount divide(Number divisor) { + return divide(BigDecimal.valueOf(divisor.doubleValue())); + } + + public Amount negate() { + return newValue(value.negate()); + } + + public Amount abs() { + return newValue(value.abs()); + } + public Amount min(Amount val) { + return (compareTo(val) <= 0 ? this : val); + } + public Amount max(Amount val) { + return (compareTo(val) >= 0 ? this : val); + } + + /* Offer related helpers */ + public BigDecimal computeQuality(Amount toExchangeThisWith) { + return value.divide(toExchangeThisWith.value, MathContext.DECIMAL128); + } + /** + * @return Amount + * The real native unit is a drop, one million of which are an XRP. + * We want `one` unit at XRP scale (1e6 drops), or if it's an IOU, + * just `one`. + */ + public Amount one() { + if (isNative()) { + return ONE_XRP; + } else { + return issue().amount(1); + } + } + + /* Serialized Type implementation */ + + @Override + public Object toJSON() { + if (isNative()) { + return toDropsString(); + } else { + return toJSONObject(); + } + } + + public JSONObject toJSONObject() { + if (isNative()) { + throw new RuntimeException("Native amounts must be serialized as a string"); + } + + JSONObject out = new JSONObject(); + out.put("currency", currencyString()); + out.put("value", valueText()); + out.put("issuer", issuerString()); + return out; + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + // TODO: probably better off using long + UInt64 man = mantissa(); + + if (isNative()) { + if (!isNegative()) { + man = man.or(BINARY_FLAG_IS_NON_NEGATIVE_NATIVE); + } + to.add(man.toByteArray()); + } else { + int exponent = exponent(); + UInt64 packed; + + if (isZero()) { + packed = BINARY_FLAG_IS_IOU; + } else if (isNegative()) { + packed = man.or(new UInt64(512 + /* 0 + */ 97 + exponent).shiftLeft(64 - 10)); + } else { + packed = man.or(new UInt64(512 + 256 + 97 + exponent).shiftLeft(64 - 10)); + } + + to.add(packed.toByteArray()); + to.add(currency.bytes()); + to.add(issuer.bytes()); + } + } + + @Override + public Type type() { + return Type.Amount; + } + + public static class Translator extends TypeTranslator { + @Override + public Amount fromString(String s) { + // We need to use the full dotted.path here, otherwise + // we get confused with the AmountField Amount + return com.ripple.core.coretypes.Amount.fromString(s); + } + + @Override + public Amount fromParser(BinaryParser parser, Integer hint) { + BigDecimal value; + byte[] mantissa = parser.read(8); + byte b1 = mantissa[0], b2 = mantissa[1]; + + boolean isIOU = (b1 & 0x80) != 0; + boolean isPositive = (b1 & 0x40) != 0; + int sign = isPositive ? 1 : -1; + + if (isIOU) { + mantissa[0] = 0; + Currency curr = Currency.fromParser(parser); + AccountID issuer = AccountID.fromParser(parser); + int exponent = ((b1 & 0x3F) << 2) + ((b2 & 0xff) >> 6) - 97; + mantissa[1] &= 0x3F; + + value = new BigDecimal(new BigInteger(sign, mantissa), -exponent); + return new Amount(value, curr, issuer, false); + } else { + mantissa[0] &= 0x3F; + value = xrpFromDropsMantissa(mantissa, sign); + return new Amount(value); + } + } + + @Override + public String toString(Amount obj) { + return obj.stringRepr(); + } + + public JSONObject toJSONObject(Amount obj) { + return obj.toJSONObject(); + } + + @Override + public Amount fromJSONObject(JSONObject jsonObject) { + String valueString = jsonObject.getString("value"); + String issuerString = jsonObject.getString("issuer"); + String currencyString = jsonObject.getString("currency"); + return new Amount(new BigDecimal(valueString), currencyString, issuerString); + } + + @Override + public Amount fromJacksonObject(ObjectNode object) { + checkField(object, "value"); + checkField(object, "issuer"); + checkField(object, "currency"); + String value = object.get("value").asText(); + String issuer = object.get("issuer").asText(); + String currency = object.get("currency").asText(); + return new Amount(new BigDecimal(value), currency, issuer); + } + + private void checkField(ObjectNode object, String field) { + if (!object.has(field)) { + throw new IllegalArgumentException(object + "is missing `" + + field + "`"); + } + } + } + static public Translator translate = new Translator(); + + public static BigDecimal xrpFromDropsMantissa(byte[] mantissa, int sign) { + return new BigDecimal(new BigInteger(sign, mantissa), 6); + } + + /* Number overides */ + @Override + public int intValue() { + return value.intValueExact(); + } + + @Override + public long longValue() { + return value.longValueExact(); + } + + @Override + public float floatValue() { + return value.floatValue(); + } + + @Override + public double doubleValue() { + return value.doubleValue(); + } + + public BigInteger bigIntegerValue() { + return value.toBigIntegerExact(); + } + + public Amount newIssuer(AccountID issuer) { + return new Amount(value, currency, issuer, isNative, unbounded); + } + + public Amount copy() { + return new Amount(value, currency, issuer, isNative, unbounded); + } + + // Static constructors + public static Amount fromString(String val) { + if (val.contains("/")) { + return fromIOUString(val); + } else if (val.contains(".")) { + return fromXrpString(val); + } else { + return fromDropString(val); + } + + } + + public static Amount fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + + public static Amount fromHex(String hex) { + return translate.fromHex(hex); + } + public static Amount fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + public static Amount fromJSONObject(JSONObject jsonObject) { + return translate.fromJSONObject(jsonObject); + } + + public static Amount fromJacksonObject(ObjectNode object) { + return translate.fromJacksonObject(object); + } + + public static Amount fromValue(Object amount) { + return translate.fromValue(amount); + } + + public static Amount fromDropString(String val) { + BigDecimal xrp = new BigDecimal(val).scaleByPowerOfTen(-6); + checkDropsValueWhole(val); + return new Amount(xrp); + } + + public static Amount fromIOUString(String val) { + String[] split = val.split("/"); + if (split.length == 1) { + throw new RuntimeException("IOU string must be in the form " + + "number/currencyString or " + + "number/currencyString/issuerString"); + } else if (split.length == 2) { + return new Amount(new BigDecimal(split[0]), split[1]); + } else { + return new Amount(new BigDecimal(split[0]), split[1], split[2]); + } + } + + @Deprecated + private static Amount fromXrpString(String valueString) { + BigDecimal val = new BigDecimal(valueString); + return new Amount(val); + } + + /** + * @return A String representation as used by ripple json format + */ + public String stringRepr() { + if (isNative()) { + return toDropsString(); + } else { + return iouTextFull(); + } + } + + public String toDropsString() { + if (!isNative()) { + throw new RuntimeException("Amount is not native"); + } + return bigIntegerDrops().toString(); + } + + private String iouText() { + return String.format("%s/%s", valueText(), currencyString()); + } + + public String iouTextFull() { + return String.format("%s/%s/%s", valueText(), currencyString(), issuerString()); + } + + public String toTextFull() { + if (isNative()) { + return nativeText(); + } else { + return iouTextFull(); + } + } + + public String nativeText() { + return String.format("%s/XRP", valueText()); + } + + @Override + public String toString() { + return toTextFull(); + } + + public String toText() { + if (isNative()) { + return nativeText(); + } else { + return iouText(); + } + } + + /** + * @return A String containing the value (in XRP scale when native) + * as a decimal number + * + */ + public String valueText() { + return value.signum() == 0 ? "0" : value().toPlainString(); + } + + private void checkLowerDropBound(BigDecimal val) { + if (val.scale() > 6) { + PrecisionError bigger = getOutOfBoundsError(val, + "smaller than min native value", + MIN_NATIVE_VALUE); + bigger.illegal = this; + throw bigger; + } + } + + private void checkUpperBound(BigDecimal val) { + if (val.compareTo(MAX_NATIVE_VALUE) > 0) { + PrecisionError bigger = getOutOfBoundsError(val, + "bigger than max native value ", + MAX_NATIVE_VALUE); + bigger.illegal = this; + throw bigger; + } + } + + private static PrecisionError getOutOfBoundsError( + BigDecimal abs, + String sized, + BigDecimal bound) { + return new PrecisionError( + abs.toPlainString() + " absolute XRP is " + sized + bound); + } + + private void checkXRPBounds() { + BigDecimal v = value.abs(); + try { + checkLowerDropBound(v); + checkUpperBound(v); + } catch (PrecisionError e) { + if (v.compareTo(TAKER_PAYS_FOR_THAT_DAMN_OFFER) == 0) { + return; + } + throw e; + } + } + + + private static int significantDigits(BigDecimal input) { + input = input.stripTrailingZeros(); + return input.scale() < 0 + ? input.precision() - input.scale() + : input.precision(); + } + + public int significantDigits() { + return significantDigits(value); + } + + public static void checkDropsValueWhole(String drops) { + boolean contains = drops.contains("."); + if (contains) { + throw new RuntimeException("Drops string contains floating point is decimal"); + } + } + + public static BigDecimal roundValue(BigDecimal value, boolean nativeSrc) { + int i = value.precision() - value.scale(); + return value.setScale(nativeSrc ? MAXIMUM_NATIVE_SCALE : + MAXIMUM_IOU_PRECISION - i, + MATH_CONTEXT.getRoundingMode()); + } + + private static BigDecimal parseDecimal(String s) { + return new BigDecimal(s.replace(",", "")); //# .scaleByPowerOfTen(6); + } + + private static AmountField amountField(final Field f) { + return new AmountField() { + @Override + public Field getField() { + return f; + } + }; + } + + static public AmountField Amount = amountField(Field.Amount); + static public AmountField Balance = amountField(Field.Balance); + static public AmountField LimitAmount = amountField(Field.LimitAmount); + static public AmountField DeliveredAmount = amountField(Field.DeliveredAmount); + static public AmountField TakerPays = amountField(Field.TakerPays); + static public AmountField TakerGets = amountField(Field.TakerGets); + static public AmountField LowLimit = amountField(Field.LowLimit); + static public AmountField HighLimit = amountField(Field.HighLimit); + static public AmountField Fee = amountField(Field.Fee); + static public AmountField SendMax = amountField(Field.SendMax); + static public AmountField DeliverMin = amountField(Field.DeliverMin); + static public AmountField MinimumOffer = amountField(Field.MinimumOffer); + static public AmountField RippleEscrow = amountField(Field.RippleEscrow); + static public AmountField taker_gets_funded = amountField(Field.taker_gets_funded); + static public AmountField taker_pays_funded = amountField(Field.taker_pays_funded); + +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Blob.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Blob.java new file mode 100644 index 0000000000..48708f78f5 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Blob.java @@ -0,0 +1,119 @@ + +package com.ripple.core.coretypes; + +import com.ripple.core.fields.BlobField; +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import com.ripple.encodings.common.B16; +import org.bouncycastle.util.encoders.Hex; + +public class Blob implements SerializedType { + public Blob(byte[] bytes) { + buffer = bytes.clone(); + } + + private final byte[] buffer; + + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + @Override + public byte[] toBytes() { + return buffer; + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + translate.toBytesSink(this, to); + } + + @Override + public Type type() { + return Type.Blob; + } + + public static Blob fromBytes(byte[] bytes) { + return new Blob(bytes); + } + + public static Blob fromHex(String hex) { + return fromBytes(B16.decode(hex)); + } + + public static Blob fromParser(BinaryParser parser, int hint) { + return translate.fromParser(parser, hint); + } + public static Blob fromParser(BinaryParser parser) { + return translate.fromParser(parser, null); + } + + public static class Translator extends TypeTranslator { + @Override + public Blob fromParser(BinaryParser parser, Integer hint) { + if (hint == null) { + hint = parser.size() - parser.pos(); + } + return new Blob(parser.read(hint)); + } + + @Override + public Object toJSON(Blob obj) { + return toString(obj); + } + + @Override + public String toString(Blob obj) { + return B16.encode(obj.buffer); + } + + @Override + public Blob fromString(String value) { + return new Blob(Hex.decode(value)); + } + + @Override + public void toBytesSink(Blob obj, BytesSink to) { + to.add(obj.buffer.clone()); + } + } + + static public Translator translate = new Translator(); + + private static BlobField blobField(final Field f) { + return new BlobField() { + @Override + public Field getField() { + return f; + } + }; + } + + static public BlobField PublicKey = blobField(Field.PublicKey); + static public BlobField MessageKey = blobField(Field.MessageKey); + static public BlobField SigningPubKey = blobField(Field.SigningPubKey); + static public BlobField TxnSignature = blobField(Field.TxnSignature); + static public BlobField MasterSignature = blobField(Field.MasterSignature); + static public BlobField Signature = blobField(Field.Signature); + static public BlobField Domain = blobField(Field.Domain); + static public BlobField FundCode = blobField(Field.FundCode); + static public BlobField RemoveCode = blobField(Field.RemoveCode); + static public BlobField ExpireCode = blobField(Field.ExpireCode); + static public BlobField CreateCode = blobField(Field.CreateCode); + + static public BlobField MemoType = blobField(Field.MemoType); + static public BlobField MemoData = blobField(Field.MemoData); + static public BlobField MemoFormat = blobField(Field.MemoFormat); + static public BlobField Condition = blobField(Field.Condition); + static public BlobField Fulfillment = blobField(Field.Fulfillment); +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Currency.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Currency.java new file mode 100644 index 0000000000..27832e335b --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Currency.java @@ -0,0 +1,250 @@ +package com.ripple.core.coretypes; + +import com.ripple.core.coretypes.hash.Hash160; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.encodings.common.B16; +import com.ripple.utils.Utils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + * Funnily enough, yes, in rippled a currency is represented by a Hash160 type. + * For the sake of consistency and convenience, this quirk is repeated here. + * + * https://gist.github.com/justmoon/8597643 + */ +public class Currency extends Hash160 { + public static final Currency NEUTRAL = new Currency(Utils.padTo160(BigInteger.ONE.toByteArray())); + public static final Currency XRP = new Currency(Utils.padTo160(BigInteger.ZERO.toByteArray())); + + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + translate.toBytesSink(this, to); + } + + public boolean isNative() { + return this == Currency.XRP || equals(Currency.XRP); + } + + public boolean isIOU() { + return !isNative(); + } + + public static enum Type { + HASH, + ISO, // three letter isoCode + DEMURRAGE, + UNKNOWN; + + public static Type fromByte(byte typeByte) { + if (typeByte == 0x00) { + return ISO; + } else if (typeByte == 0x01) { + return DEMURRAGE; + } else if ((typeByte & 0x80) != 0) { + return HASH; + } else { + return UNKNOWN; + } + } + } + Type type; + + public static class Demurrage { + Date interestStart; + String isoCode; + double interestRate; + + static public BigDecimal applyRate(BigDecimal amount, BigDecimal rate, TimeUnit time, long units) { + BigDecimal appliedRate = getSeconds(time, units).divide(rate, MathContext.DECIMAL64); + BigDecimal factor = BigDecimal.valueOf(Math.exp(appliedRate.doubleValue())); + return amount.multiply(factor, MathContext.DECIMAL64); + } + + static public BigDecimal calculateRate(BigDecimal rate, TimeUnit time, long units) { + BigDecimal seconds = getSeconds(time, units); + BigDecimal log = ln(rate); + return seconds.divide(log, MathContext.DECIMAL64); + } + + private static BigDecimal ln(BigDecimal bd) { + return BigDecimal.valueOf(Math.log(bd.doubleValue())); + } + + private static BigDecimal getSeconds(TimeUnit time, long units) { + return BigDecimal.valueOf(time.toSeconds(units)); + } + + public Demurrage(byte[] bytes) { + BinaryParser parser = new BinaryParser(bytes); + parser.skip(1); // The type + isoCode = isoCodeFromBytesAndOffset(parser.read(3), 0);// The isoCode + interestStart = RippleDate.fromParser(parser); + long l = UInt64.fromParser(parser).longValue(); + interestRate = Double.longBitsToDouble(l); + } + } + public Demurrage demurrage = null; + public Currency(byte[] bytes) { + super(bytes); + type = Type.fromByte(this.hash[0]); + if (type == Type.DEMURRAGE) { + demurrage = new Demurrage(bytes); + } + } + + /** + * It's better to extend HashTranslator than the Hash160.Translator directly + * That way the generics can still vibe with the @Override + */ + private static class CurrencyTranslator extends HashTranslator { + @Override + public int byteWidth() { + return 20; + } + + @Override + public Currency newInstance(byte[] b) { + return new Currency(b); + } + + @Override + public Object toJSON(Currency obj) { + return obj.toString(); + } + + @Override + public Currency fromString(String value) { + if (value.length() == 40 /* byteWidth() * 2 */) { + return newInstance(B16.decode(value)); + } else if (value.equals("XRP")) { + return XRP; + } else { + if (!(value.length() == 3)) { +// if (!value.matches("[A-Z0-9]{3}")) { + throw new RuntimeException("Currency code must be 3 characters"); + } + return newInstance(encodeCurrency(value)); + } + } + } + + public static Currency fromString(String currency) { + return translate.fromString(currency); + } + + public static Currency fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + + @Override + public String toString() { + switch (type) { + case ISO: + String code = getCurrencyCodeFromTLCBytes(bytes()); + if (code.equals("XRP")) { + // HEX of the bytes + return super.toString(); + } else if (code.equals("\0\0\0")) { + return "XRP"; + } else { + // the 3 letter isoCode + return code; + } + case HASH: + case DEMURRAGE: + case UNKNOWN: + default: + return super.toString(); + } + } + + public String humanCode() { + if (type == Type.ISO) { + return getCurrencyCodeFromTLCBytes(hash); + } else if (type == Type.DEMURRAGE) { + return isoCodeFromBytesAndOffset(hash, 1); + } else { + throw new IllegalStateException("No human code for currency of type " + type); + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Currency) { + Currency other = (Currency) obj; + byte[] bytes = this.bytes(); + byte[] otherBytes = other.bytes(); + + if (type == Type.ISO && other.type == Type.ISO) { + return (bytes[12] == otherBytes[12] && + bytes[13] == otherBytes[13] && + bytes[14] == otherBytes[14]); + } + } + return super.equals(obj); // Full comparison + } + + private static CurrencyTranslator translate = new CurrencyTranslator(); + + /* + * The following are static methods, legacy from when there was no + * usage of Currency objects, just String with "XRP" ambiguity. + * */ + public static byte[] encodeCurrency(String currencyCode) { + byte[] currencyBytes = new byte[20]; + currencyBytes[12] = (byte) currencyCode.codePointAt(0); + currencyBytes[13] = (byte) currencyCode.codePointAt(1); + currencyBytes[14] = (byte) currencyCode.codePointAt(2); + return currencyBytes; + } + + public static String getCurrencyCodeFromTLCBytes(byte[] bytes) { + int i; + boolean zeroInNonCurrencyBytes = true; + + for (i = 0; i < 20; i++) { + zeroInNonCurrencyBytes = zeroInNonCurrencyBytes && + ((i == 12 || i == 13 || i == 14) || // currency bytes (0 or any other) + bytes[i] == 0); // non currency bytes (0) + } + + if (zeroInNonCurrencyBytes) { + return isoCodeFromBytesAndOffset(bytes, 12); + } else { + throw new IllegalStateException("Currency is invalid"); + } + } + + private static char charFrom(byte[] bytes, int i) { + return (char) bytes[i]; + } + + private static String isoCodeFromBytesAndOffset(byte[] bytes, int offset) { + char a = charFrom(bytes, offset); + char b = charFrom(bytes, offset + 1); + char c = charFrom(bytes, offset + 2); + return "" + a + b + c; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Flags.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Flags.java new file mode 100644 index 0000000000..9b0c319dcf --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Flags.java @@ -0,0 +1,35 @@ +package com.ripple.core.coretypes; + +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; + +import java.util.BitSet; + +// TODO +public class Flags extends BitSet implements SerializedType { + @Override + public Object toJSON() { + return null; + } + + @Override + public byte[] toBytes() { + return new byte[0]; + } + + @Override + public String toHex() { + return null; + } + + @Override + public void toBytesSink(BytesSink to) { + + } + + @Override + public Type type() { + return Type.UInt32; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Issue.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Issue.java new file mode 100644 index 0000000000..44f75dae82 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Issue.java @@ -0,0 +1,101 @@ +package com.ripple.core.coretypes; + +import com.ripple.core.coretypes.hash.Hash160; +import org.json.JSONObject; + +import java.math.BigDecimal; + +/** + * Represents a currency/issuer pair + */ +public class Issue implements Comparable { + + public static final Issue XRP = fromString("XRP"); + final Currency currency; + final AccountID issuer; + + public Issue(Currency currency, AccountID issuer) { + this.currency = currency; + this.issuer = issuer; + } + + public static Issue fromString(String pair) { + String[] split = pair.split("/"); + return fromStringPair(split); + } + + private static Issue fromStringPair(String[] split) { + if (split.length == 2) { + return new Issue(Currency.fromString(split[0]), AccountID.fromString(split[1])); + } else if (split[0].equals("XRP")) { + return new Issue(Currency.XRP, AccountID.XRP_ISSUER); + } else { + throw new RuntimeException("Issue string must be XRP or $currency/$issuer"); + } + } + + /** + * See {@link com.ripple.core.fields.Field#TakerGetsCurrency} + * See {@link com.ripple.core.fields.Field#TakerGetsIssuer} + * + * TODO: better handling of Taker(Gets|Pays)(Issuer|Curency) + * maybe special subclasses of AccountID / Currency + * respectively? + */ + public static Issue from160s(Hash160 currency, Hash160 issuer) { + return new Issue(new Currency(currency.bytes()), + new AccountID(issuer.toBytes())); + } + + public Currency currency() { + return currency; + } + + public AccountID issuer() { + return issuer; + } + + @Override + public String toString() { + if (isNative()) { + return "XRP"; + } else { + return String.format("%s/%s", currency, issuer); + } + } + + public JSONObject toJSON() { + JSONObject o = new JSONObject(); + o.put("currency", currency); + if (!isNative()) { + o.put("issuer", issuer); + } + return o; + } + + public Amount amount(BigDecimal value) { + return new Amount(value, currency, issuer, isNative()); + } + + public boolean isNative() { + return this == XRP || currency.equals(Currency.XRP); + } + + public Amount amount(Number value) { + return new Amount(BigDecimal.valueOf(value.doubleValue()), currency, issuer, isNative()); + } + + @Override + public int compareTo(Issue o) { + int ret = issuer.compareTo(o.issuer); + if (ret != 0) { + return ret; + } + ret = currency.compareTo(o.currency); + return ret; + } + + public Amount roundedAmount(BigDecimal amount) { + return amount(Amount.roundValue(amount, isNative())); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/IssuePair.java b/ripple-core/src/main/java/com/ripple/core/coretypes/IssuePair.java new file mode 100644 index 0000000000..0196e67f86 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/IssuePair.java @@ -0,0 +1,36 @@ +package com.ripple.core.coretypes; + +import org.json.JSONObject; + +import java.text.MessageFormat; + +public class IssuePair implements Comparable { + public final Issue pays; + public final Issue gets; + + public IssuePair(Issue pays, Issue gets) { + this.pays = pays; + this.gets = gets; + } + + @Override + public String toString() { + return MessageFormat.format("{0}/{1}", pays, gets); + } + + @Override + public int compareTo(IssuePair o) { + int cmp = pays.compareTo(o.pays); + if (cmp == 0) { + cmp = gets.compareTo(o.gets); + } + return cmp; + } + + public JSONObject toJSON() { + JSONObject ret = new JSONObject(); + ret.put("taker_gets", gets.toJSON()); + ret.put("taker_pays", pays.toJSON()); + return ret; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/PathSet.java b/ripple-core/src/main/java/com/ripple/core/coretypes/PathSet.java new file mode 100644 index 0000000000..b054229787 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/PathSet.java @@ -0,0 +1,273 @@ +package com.ripple.core.coretypes; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.ripple.core.fields.Field; +import com.ripple.core.fields.PathSetField; +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; + +public class PathSet extends ArrayList implements SerializedType { + public static byte PATH_SEPARATOR_BYTE = (byte) 0xFF; + public static byte PATHSET_END_BYTE = (byte) 0x00; + + public PathSet(){} + + public static PathSet fromJSONArray(JSONArray array) { + return translate.fromJSONArray(array); + } + + public static PathSet fromHex(String hex) { + return translate.fromHex(hex); + } + + public static class Hop { + public static byte TYPE_ACCOUNT = (byte) 0x01; + public static byte TYPE_CURRENCY = (byte) 0x10; + public static byte TYPE_ISSUER = (byte) 0x20; + public static final int TYPE_ACCOUNT_CURRENCY_ISSUER = TYPE_CURRENCY | TYPE_ACCOUNT | TYPE_ISSUER; + public static final int TYPE_ACCOUNT_CURRENCY = TYPE_CURRENCY | TYPE_ACCOUNT; + public static int VALID_TYPE_MASK = ~(TYPE_ACCOUNT | TYPE_CURRENCY | TYPE_ISSUER); + + public AccountID account; + public AccountID issuer; + public Currency currency; + private int type; + + public boolean hasIssuer() { + return issuer != null; + } + public boolean hasCurrency() { + return currency != null; + } + public boolean hasAccount() { + return account != null; + } + + public int getType() { + if (type == 0) { + synthesizeType(); + } + return type; + } + + static public Hop fromJSONObject(JSONObject json) { + Hop hop = new Hop(); + if (json.has("account")) { + hop.account = AccountID.fromAddress(json.getString("account")); + } + if (json.has("issuer")) { + hop.issuer = AccountID.fromAddress(json.getString("issuer")); + } + if (json.has("currency")) { + hop.currency = Currency.fromString(json.getString("currency")); + } + if (json.has("type")) { + hop.type = json.getInt("type"); + } + return hop; + } + + public void synthesizeType() { + type = 0; + + if (hasAccount()) type |= TYPE_ACCOUNT; + if (hasCurrency()) type |= TYPE_CURRENCY; + if (hasIssuer()) type |= TYPE_ISSUER; + } + + public JSONObject toJSONObject() { + JSONObject object = new JSONObject(); + object.put("type", getType()); + + if (hasAccount()) object.put("account", account.toJSON()); + if (hasIssuer()) object.put("issuer", issuer.toJSON()); + if (hasCurrency()) object.put("currency", currency.toJSON()); + return object; + } + + public static Hop fromJacksonObject(ObjectNode json) { + Hop hop = new Hop(); + if (json.has("account")) { + hop.account = AccountID.fromAddress(json.get("account").asText()); + } + if (json.has("issuer")) { + hop.issuer = AccountID.fromAddress(json.get("issuer").asText()); + } + if (json.has("currency")) { + hop.currency = Currency.fromString(json.get("currency").asText()); + } + if (json.has("type")) { + hop.type = json.get("type").asInt(); + } + return hop; + + } + } + public static class Path extends ArrayList { + static public Path fromJSONArray(JSONArray array) { + Path path = new Path(); + int nHops = array.length(); + for (int i = 0; i < nHops; i++) { + JSONObject hop = array.getJSONObject(i); + path.add(Hop.fromJSONObject(hop)); + } + + return path; + } + static public Path fromJacksonArray(ArrayNode array) { + Path path = new Path(); + int nHops = array.size(); + for (int i = 0; i < nHops; i++) { + ObjectNode hop = (ObjectNode) array.get(i); + path.add(Hop.fromJacksonObject(hop)); + } + return path; + } + public JSONArray toJSONArray() { + JSONArray array = new JSONArray(); + for (Hop hop : this) { + array.put(hop.toJSONObject()); + } + return array; + } + } + + public JSONArray toJSONArray() { + JSONArray array = new JSONArray(); + for (Path path : this) { + array.put(path.toJSONArray()); + } + return array; + } + + // SerializedType interface implementation + @Override + public Object toJSON() { + return toJSONArray(); + } + + @Override + public void toBytesSink(BytesSink buffer) { + int n = 0; + for (Path path : this) { + if (n++ != 0) { + buffer.add(PATH_SEPARATOR_BYTE); + } + for (Hop hop : path) { + int type = hop.getType(); + buffer.add((byte) type); + if (hop.hasAccount()) { + buffer.add(hop.account.bytes()); + } + if (hop.hasCurrency()) { + buffer.add(hop.currency.bytes()); + } + if (hop.hasIssuer()) { + buffer.add(hop.issuer.bytes()); + } + } + } + buffer.add(PATHSET_END_BYTE); + } + + @Override + public Type type() { + return Type.PathSet; + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + + public static class Translator extends TypeTranslator { + @Override + public PathSet fromParser(BinaryParser parser, Integer hint) { + PathSet pathSet = new PathSet(); + PathSet.Path path = null; + while (!parser.end()) { + byte type = parser.readOne(); + if (type == PATHSET_END_BYTE) { + break; + } + if (path == null) { + path = new PathSet.Path(); + pathSet.add(path); + } + if (type == PATH_SEPARATOR_BYTE) { + path = null; + continue; + } + + PathSet.Hop hop = new PathSet.Hop(); + path.add(hop); + if ((type & Hop.TYPE_ACCOUNT) != 0) { + hop.account = AccountID.fromParser(parser); + } + if ((type & Hop.TYPE_CURRENCY) != 0) { + hop.currency = Currency.fromParser(parser); + } + if ((type & Hop.TYPE_ISSUER) != 0) { + hop.issuer = AccountID.fromParser(parser); + } + } + + return pathSet; + } + + @Override + public PathSet fromJSONArray(JSONArray array) { + PathSet paths = new PathSet(); + + int nPaths = array.length(); + + for (int i = 0; i < nPaths; i++) { + JSONArray path = array.getJSONArray(i); + paths.add(Path.fromJSONArray(path)); + } + return paths; + + } + + @Override + public PathSet fromJacksonArray(ArrayNode array) { + PathSet paths = new PathSet(); + + int nPaths = array.size(); + + for (int i = 0; i < nPaths; i++) { + ArrayNode path = (ArrayNode) array.get(i); + paths.add(Path.fromJacksonArray(path)); + } + + return paths; + } + } + static public Translator translate = new Translator(); + + public static PathSet fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + public static PathSet fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + private static PathSetField pathsetField(final Field f) { + return new PathSetField(){ @Override public Field getField() {return f;}}; + } + static public PathSetField Paths = pathsetField(Field.Paths); +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Quality.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Quality.java new file mode 100644 index 0000000000..212da6d78e --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Quality.java @@ -0,0 +1,26 @@ +package com.ripple.core.coretypes; + +import com.ripple.core.coretypes.hash.Hash256; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public class Quality { + /** + * Finds the quality (TakerPays/TakerGets) ratio packed into the last 64 + * bits of root DirectoryNode ledger indexes. + */ + public static BigDecimal fromBookDirectory(Hash256 bookDirectory, + boolean payIsNative, + boolean getIsNative) { + // The last 7 bytes contains the mantissa + byte[] mantissa = bookDirectory.slice(-7); + // Most significant byte has the exponent packed + int exponent = ( bookDirectory.get(-8) & 0xFF) - 100; + // Return the value in XRP scale, rather than drops, as stored. + int scale = -(payIsNative ? exponent - 6 : + getIsNative ? exponent + 6 : exponent); + BigInteger unsignedBig = new BigInteger(1, mantissa); + return new BigDecimal(unsignedBig, scale); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/RippleDate.java b/ripple-core/src/main/java/com/ripple/core/coretypes/RippleDate.java new file mode 100644 index 0000000000..71a94e1ce9 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/RippleDate.java @@ -0,0 +1,82 @@ +package com.ripple.core.coretypes; + +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.BinaryParser; + +import java.text.SimpleDateFormat; +import java.util.*; + +//public class RippleDate extends Date implements SerializedType { +public class RippleDate extends Date { + private static final SimpleDateFormat sdf = new SimpleDateFormat(); + + public static long RIPPLE_EPOCH_SECONDS_OFFSET = 0x386D4380; + static { + sdf.setTimeZone(new SimpleTimeZone(0, "GMT")); + sdf.applyPattern("dd MMM yyyy HH:mm:ss z"); + + /** + * Magic constant tested and documented. + * + * Seconds since the unix epoch from unix time (accounting leap years etc) + * at 1/January/2000 GMT + */ + GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); + cal.set(2000, Calendar.JANUARY, 1, 0, 0, 0); + long computed = cal.getTimeInMillis() / 1000; + assertEquals("01 Jan 2000 00:00:00 GMT", sdf.format(cal.getTime())); // TODO + assertEquals(RippleDate.RIPPLE_EPOCH_SECONDS_OFFSET, computed); + } + + static public String gmtString(Date date) { + return sdf.format(date); + } + + private static void assertEquals(String s, String s1) { + if (!s.equals(s1)) throw new AssertionError(String.format("%s != %s", s, s1)); + } + private static void assertEquals(long a, long b) { + if (a != b) throw new AssertionError(String.format("%s != %s", a, b)); + } + + private RippleDate() { + super(); + } + private RippleDate(long milliseconds) { + super(milliseconds); + } + + public long secondsSinceRippleEpoch() { + return ((this.getTime() / 1000) - RIPPLE_EPOCH_SECONDS_OFFSET); + } + public static RippleDate fromSecondsSinceRippleEpoch(Number seconds) { + return new RippleDate((seconds.longValue() + RIPPLE_EPOCH_SECONDS_OFFSET) * 1000); + } + public static RippleDate fromParser(BinaryParser parser) { + UInt32 uInt32 = UInt32.fromParser(parser); + return fromSecondsSinceRippleEpoch(uInt32); + } + public static RippleDate now() { + return new RippleDate(); + } + +/* @Override + public Object toJSON() { + return secondsSinceRippleEpoch(); + } + + @Override + public byte[] toBytes() { + return new UInt32(secondsSinceRippleEpoch()).toBytes(); + } + + @Override + public String toHex() { + return new UInt32(secondsSinceRippleEpoch()).toHex(); + } + + @Override + public void toBytesSink(BytesSink to) { + new UInt32(secondsSinceRippleEpoch()).toBytesSink(to); + }*/ +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/STArray.java b/ripple-core/src/main/java/com/ripple/core/coretypes/STArray.java new file mode 100644 index 0000000000..6e7b0191f3 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/STArray.java @@ -0,0 +1,129 @@ +package com.ripple.core.coretypes; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.ripple.core.fields.Field; +import com.ripple.core.fields.STArrayField; +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; + +public class STArray extends ArrayList implements SerializedType { + + public JSONArray toJSONArray() { + JSONArray array = new JSONArray(); + + for (STObject so : this) { + array.put(so.toJSON()); + } + + return array; + } + + @Override + public Object toJSON() { + return toJSONArray(); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + for (STObject stObject : this) { + stObject.toBytesSink(to); + } + } + + @Override + public Type type() { + return Type.STArray; + } + + public static STArray fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + public static STArray fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + public static STArray fromHex(String hex) { + return translate.fromHex(hex); + } + + public static class Translator extends TypeTranslator { + @Override + public STArray fromParser(BinaryParser parser, Integer hint) { + STArray stArray = new STArray(); + while (!parser.end()) { + Field field = parser.readField(); + if (field == Field.ArrayEndMarker) { + break; + } + STObject outer = new STObject(); + // assert field.getType() == Type.STObject; + outer.put(field, STObject.fromParser(parser)); + stArray.add(STObject.formatted(outer)); + } + return stArray; + } + + public JSONArray toJSONArray(STArray obj) { + return obj.toJSONArray(); + } + + @Override + public STArray fromJSONArray(JSONArray jsonArray) { + STArray arr = new STArray(); + + for (int i = 0; i < jsonArray.length(); i++) { + Object o = jsonArray.get(i); + arr.add(STObject.fromJSONObject((JSONObject) o)); + } + + return arr; + } + @Override + public STArray fromJacksonArray(ArrayNode jsonArray) { + STArray arr = new STArray(); + + for (int i = 0; i < jsonArray.size(); i++) { + ObjectNode object = (ObjectNode) jsonArray.get(i); + arr.add(STObject.fromJacksonObject(object)); + } + + return arr; + } + } + static public Translator translate = new Translator(); + + public STArray(){} + + private static STArrayField starrayField(final Field f) { + return new STArrayField(){ @Override public Field getField() {return f;}}; + } + + static public STArrayField AffectedNodes = starrayField(Field.AffectedNodes); + static public STArrayField SignerEntries = starrayField(Field.SignerEntries); + static public STArrayField Signers = starrayField(Field.Signers); + + static public STArrayField Template = starrayField(Field.Template); + static public STArrayField Necessary = starrayField(Field.Necessary); + static public STArrayField Sufficient = starrayField(Field.Sufficient); + static public STArrayField Majorities = starrayField(Field.Majorities); + static public STArrayField Memos = starrayField(Field.Memos); + static public STArrayField ArrayEndMarker = starrayField(Field.ArrayEndMarker); +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/STObject.java b/ripple-core/src/main/java/com/ripple/core/coretypes/STObject.java new file mode 100644 index 0000000000..b02988c082 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/STObject.java @@ -0,0 +1,489 @@ +package com.ripple.core.coretypes; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.ripple.core.coretypes.hash.HalfSha512; +import com.ripple.core.coretypes.hash.Hash128; +import com.ripple.core.coretypes.hash.Hash160; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.uint.UInt16; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.coretypes.uint.UInt8; +import com.ripple.core.fields.*; +import com.ripple.core.formats.Format; +import com.ripple.core.formats.LEFormat; +import com.ripple.core.formats.TxFormat; +import com.ripple.core.serialized.*; +import com.ripple.core.serialized.enums.EngineResult; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.serialized.enums.TransactionType; +import org.json.JSONObject; + +import java.util.EnumMap; +import java.util.Iterator; +import java.util.TreeMap; +import java.util.function.Predicate; + +public class STObject implements SerializedType, Iterable { + // Internally the fields are stored in a TreeMap + public static class FieldsMap extends TreeMap {} +// public static class FieldsMap extends HashMap {} + + protected FieldsMap fields; + public Format format; + + public STObject() { + fields = new FieldsMap(); + } + public STObject(FieldsMap fieldsMap) { + fields = fieldsMap; + } + + public static STObject fromJSON(String json) { + return fromJSONObject(new JSONObject(json)); + } + public static STObject fromJSONObject(JSONObject json) { + return translate.fromJSONObject(json); + } + public static STObject fromJacksonObject(ObjectNode object) { + return translate.fromJacksonObject(object); + } + public static STObject fromHex(String hex) { + return STObject.translate.fromHex(hex); + } + public static STObject fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + public static STObject fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + public static STObject fromParser(BinaryParser parser, Integer hint) { + return translate.fromParser(parser, hint); + } + + @Override + public Iterator iterator() { +// return fields.keySet().stream().sorted().iterator(); + return fields.keySet().iterator(); + } + + public String prettyJSON() { + return translate.toJSONObject(this).toString(2); + } + + /** + * @return a subclass of STObject using the same fields + * + * If the object has a TransactionType or LedgerEntryType + * then we can up)grade to a child class, with more specific + * helper methods, and we can use `instanceof` to great effect. + */ + public static STObject formatted(STObject source) { + return STObjectFormatter.format(source); + + } + + public Format getFormat() { + if (format == null) computeFormat(); + return format; + } + + public static class FormatException extends RuntimeException { + FormatException(String s) { + super(s); + } + } + + public void checkFormat() { + Format fmt = getFormat(); + EnumMap requirements = fmt.requirements(); + for (Field field : this) { + if (!requirements.containsKey(field)) { + throw new FormatException(fmt.name() + + " doesn't have field: " + field); + } + } + for (Field field : requirements.keySet()) { + Format.Requirement req = requirements.get(field); + if (!has(field)) { + if (req == Format.Requirement.REQUIRED) { + throw new FormatException(fmt.name() + + " requires " + field + " of type " + + field.getType()); + } + } else { + SerializedType type = get(field); + if (type.type() != field.getType()) { + if (!(field.getType() == Type.Hash160 && + type.type() == Type.AccountID)) { + throw new FormatException(type.toString() + + " is not " + field.getType()); + } + } + } + } + } + + public void setFormat(Format format) { + this.format = format; + } + + private void computeFormat() { + UInt16 tt = get(UInt16.TransactionType); + if (tt != null) { + setFormat(TxFormat.fromNumber(tt)); + } + UInt16 let = get(UInt16.LedgerEntryType); + if (let != null) { + setFormat(LEFormat.fromNumber(let)); + } + } + + public FieldsMap getFields() { + return fields; + } + + public SerializedType get(Field field) { + return fields.get(field); + } + + protected Hash256 signingHash(HashPrefix txSign) { + HalfSha512 signing = HalfSha512.prefixed256(txSign); + toBytesSink(signing, Field::isSigningField); + return signing.finish(); + } + + protected byte[] signingData(HashPrefix txSign) { + BytesList bl = new BytesList(); + bl.add(txSign.bytes()); + toBytesSink(bl, Field::isSigningField); + return bl.bytes(); + } + + protected static EngineResult engineResult(STObject obj) { + return (EngineResult) obj.get(Field.TransactionResult); + } + + static public LedgerEntryType ledgerEntryType(STObject obj) { + return (LedgerEntryType) obj.get(Field.LedgerEntryType); + } + + public static TransactionType transactionType(STObject obj) { + return (TransactionType) obj.get(Field.TransactionType); + } + + public SerializedType remove(Field f) { + return fields.remove(f); + } + + public boolean has(Field f) { + return fields.containsKey(f); + } + + public boolean has(T hf) { + return has(hf.getField()); + } + + public void put (UInt8Field f, UInt8 o) {put(f.getField(), o);} + public void put (Vector256Field f, Vector256 o) {put(f.getField(), o);} + public void put (BlobField f, Blob o) {put(f.getField(), o);} + public void put (UInt64Field f, UInt64 o) {put(f.getField(), o);} + public void put (UInt32Field f, UInt32 o) {put(f.getField(), o);} + public void put (UInt16Field f, UInt16 o) {put(f.getField(), o);} + public void put (PathSetField f, PathSet o) {put(f.getField(), o);} + public void put (STObjectField f, STObject o) {put(f.getField(), o);} + public void put (Hash256Field f, Hash256 o) {put(f.getField(), o);} + public void put (Hash160Field f, Hash160 o) {put(f.getField(), o);} + public void put (Hash128Field f, Hash128 o) {put(f.getField(), o);} + public void put (STArrayField f, STArray o) {put(f.getField(), o);} + public void put (AmountField f, Amount o) {put(f.getField(), o);} + public void put (AccountIDField f, AccountID o) {put(f.getField(), o);} + + public void putTranslated(T f, Object value) { + putTranslated(f.getField(), value); + } + + public STObject as(T f, Object value) { + putTranslated(f.getField(), value); + return this; + } + + public void put(Field f, SerializedType value) { + fields.put(f, value); + } + + public void putTranslated(Field f, Object value) { + TypeTranslator typeTranslator = Translators.forField(f); + SerializedType st; + try { + st = typeTranslator.fromValue(value); + } catch (Exception e) { + throw new RuntimeException("Couldn't put `" +value+ "` into field `" + f + "`", e); + } + fields.put(f, st); + } + + public AccountID get(AccountIDField f) { + return (AccountID) get(f.getField()); + } + + public Amount get(AmountField f) { + return (Amount) get(f.getField()); + } + + public STArray get(STArrayField f) { + return (STArray) get(f.getField()); + } + + public Hash128 get(Hash128Field f) { + return (Hash128) get(f.getField()); + } + + public Hash160 get(Hash160Field f) { + return (Hash160) get(f.getField()); + } + + public Hash256 get(Hash256Field f) { + return (Hash256) get(f.getField()); + } + + public STObject get(STObjectField f) { + return (STObject) get(f.getField()); + } + + public PathSet get(PathSetField f) { + return (PathSet) get(f.getField()); + } + + public UInt16 get(UInt16Field f) { + return (UInt16) get(f.getField()); + } + + public UInt32 get(UInt32Field f) { + return (UInt32) get(f.getField()); + } + + public UInt64 get(UInt64Field f) { + return (UInt64) get(f.getField()); + } + + public UInt8 get(UInt8Field f) { + return (UInt8) get(f.getField()); + } + + public Vector256 get(Vector256Field f) { + return (Vector256) get(f.getField()); + } + + public Blob get(BlobField f) { + return (Blob) get(f.getField()); + } + + // SerializedTypes implementation + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + public JSONObject toJSONObject() { + return translate.toJSONObject(this); + } + + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + public void toBytesSink(BytesSink to, Predicate p) { + BinarySerializer serializer = new BinarySerializer(to); + + for (Field field : this) { + if (p.test(field)) { + SerializedType value = fields.get(field); + serializer.add(field, value); + } + } + } + @Override + public void toBytesSink(BytesSink to) { + toBytesSink(to, field -> field.isSerialized()); + } + + @Override + public Type type() { + return Type.STObject; + } + + private static class Translator extends TypeTranslator { + + @Override + public STObject fromParser(BinaryParser parser, Integer hint) { + STObject so = new STObject(); + TypeTranslator tr; + SerializedType st; + Field field; + Integer sizeHint; + + // hint, is how many bytes to parse + if (hint != null) { + // end hint + hint = parser.pos() + hint; + } + + while (!parser.end(hint)) { + field = parser.readField(); + if (field == Field.ObjectEndMarker) { + break; + } + tr = Translators.forField(field); + sizeHint = field.isVLEncoded() ? parser.readVLLength() : null; + st = tr.fromParser(parser, sizeHint); + if (st == null) { + throw new IllegalStateException("Parsed " + field + " as null"); + } + so.put(field, st); + } + + return STObject.formatted(so); + } + + @Override + public Object toJSON(STObject obj) { + return toJSONObject(obj); + } + + public JSONObject toJSONObject(STObject obj) { + JSONObject json = new JSONObject(); + + for (Field f : obj) { + SerializedType obj1 = obj.get(f); + Object object = obj1.toJSON(); + json.put(f.name(), object); + } + + return json; + } + + @Override + public STObject fromJSONObject(JSONObject jsonObject) { + STObject so = new STObject(); + + Iterator keys = jsonObject.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + Object value = jsonObject.get(key); + Field fieldKey = Field.fromString(key); + if (fieldKey == null) { + continue; + } + so.putTranslated(fieldKey, value); + } + return STObject.formatted(so); + } + + @Override + public STObject fromJacksonObject(ObjectNode object) { + STObject so = new STObject(); + + Iterator keys = object.fieldNames(); + while (keys.hasNext()) { + String key = keys.next(); + JsonNode value = object.get(key); + Field fieldKey = Field.fromString(key); + if (fieldKey == null) { + continue; + } + so.putTranslated(fieldKey, value); + } + return STObject.formatted(so); + } + } + + public int size() { + return fields.size(); + } + + static private Translator translate = new Translator(); + + private static STObjectField stobjectField(final Field f) { + return new STObjectField() {@Override public Field getField() {return f; } }; + } + + static public STObjectField TransactionMetaData = stobjectField(Field.TransactionMetaData); + static public STObjectField CreatedNode = stobjectField(Field.CreatedNode); + static public STObjectField DeletedNode = stobjectField(Field.DeletedNode); + static public STObjectField ModifiedNode = stobjectField(Field.ModifiedNode); + static public STObjectField PreviousFields = stobjectField(Field.PreviousFields); + static public STObjectField FinalFields = stobjectField(Field.FinalFields); + static public STObjectField NewFields = stobjectField(Field.NewFields); + static public STObjectField TemplateEntry = stobjectField(Field.TemplateEntry); + static public STObjectField Signer = stobjectField(Field.Signer); + static public STObjectField SignerEntry = stobjectField(Field.SignerEntry); + static public STObjectField ObjectEndMarker = stobjectField(Field.ObjectEndMarker); + static public STObjectField Memo = stobjectField(Field.Memo); + static public STObjectField Majority = stobjectField(Field.Majority); + + private static class Translators { + @SuppressWarnings("unused") + private static TypeTranslator get(Class kls) { + try { + java.lang.reflect.Field translate = kls.getDeclaredField("translate"); + translate.setAccessible(true); + return (TypeTranslator) translate.get(kls); + } catch (Exception e) { + throw new RuntimeException("for kls: " + kls.getSimpleName(), e); + } + } + + private static TypeTranslator forType(Type type) { + switch (type) { + case STObject: return STObject.translate; // get(STObject.class); + case Amount: return Amount.translate; // get(Amount.class); + case UInt16: return UInt16.translate; // get(UInt16.class); + case UInt32: return UInt32.translate; // get(UInt32.class); + case UInt64: return UInt64.translate; // get(UInt64.class); + case Hash128: return Hash128.translate; // get(Hash128.class); + case Hash256: return Hash256.translate; // get(Hash256.class); + case Blob: return Blob.translate; // get(Blob.class); + case AccountID: return AccountID.translate; // get(AccountID.class); + case STArray: return STArray.translate; // get(STArray.class); + case UInt8: return UInt8.translate; // get(UInt8.class); + case Hash160: return Hash160.translate; // get(Hash160.class); + case PathSet: return PathSet.translate; // get(PathSet.class); + case Vector256: return Vector256.translate; // get(Vector256.class); + default: throw new IllegalStateException("Unknown type"); + } + } + + private static TypeTranslator forField(Field field) { + if (field.tag == null) { + switch (field) { + case LedgerEntryType: + field.tag = LedgerEntryType.translate; //get(LedgerEntryType.class); + break; + case TransactionType: + field.tag = TransactionType.translate;// get(TransactionType.class); + break; + case TransactionResult: + field.tag = EngineResult.translate; // get(EngineResult.class); + break; + default: + field.tag = forType(field.getType()); + break; + } + } + return getCastedTag(field); + } + + @SuppressWarnings("unchecked") + private static TypeTranslator getCastedTag(Field field) { + return (TypeTranslator) field.tag; + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/STObjectFormatter.java b/ripple-core/src/main/java/com/ripple/core/coretypes/STObjectFormatter.java new file mode 100644 index 0000000000..d17b6ae0ee --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/STObjectFormatter.java @@ -0,0 +1,170 @@ +package com.ripple.core.coretypes; + +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.generic.Validation; +import com.ripple.core.types.known.sle.LedgerHashes; +import com.ripple.core.types.known.sle.entries.*; +import com.ripple.core.types.known.tx.result.AffectedNode; +import com.ripple.core.types.known.tx.result.TransactionMeta; +import com.ripple.core.types.known.tx.txns.*; +import com.ripple.core.types.known.tx.txns.pseudo.EnableAmendment; +import com.ripple.core.types.known.tx.txns.pseudo.SetFee; + +public class STObjectFormatter { + public static STObject format(STObject source) { + // This would need to go before the test that just checks + // for ledgerEntryType + if (AffectedNode.isAffectedNode(source)) { + return new AffectedNode(source); + } + + if (TransactionMeta.isTransactionMeta(source)) { + TransactionMeta meta = new TransactionMeta(); + meta.fields = source.fields; + return meta; + } + + LedgerEntryType ledgerEntryType = STObject.ledgerEntryType(source); + if (ledgerEntryType != null) { + return ledgerFormatted(source, ledgerEntryType); + } + + TransactionType transactionType = STObject.transactionType(source); + if (transactionType != null) { + return transactionFormatted(source, transactionType); + } + + if (Validation.isValidation(source)) { + Validation validation = new Validation(); + validation.fields = source.fields; + return validation; + } + + return source; + } + + private static STObject transactionFormatted(STObject source, TransactionType transactionType) { + STObject constructed = null; + switch (transactionType) { + case Payment: + constructed = new Payment(); + break; + case EscrowCreate: + constructed = new EscrowCreate(); + break; + case EscrowFinish: + constructed = new EscrowFinish(); + break; + case AccountSet: + constructed = new AccountSet(); + break; + case EscrowCancel: + constructed = new EscrowCancel(); + break; + case SetRegularKey: + constructed = new SetRegularKey(); + break; + case OfferCreate: + constructed = new OfferCreate(); + break; + case OfferCancel: + constructed = new OfferCancel(); + break; + case TicketCreate: + constructed = new TicketCreate(); + break; + case TicketCancel: + constructed = new TicketCancel(); + break; + case SignerListSet: + constructed = new SignerListSet(); + break; + case PaymentChannelCreate: + constructed = new PaymentChannelCreate(); + break; + case PaymentChannelFund: + constructed = new PaymentChannelFund(); + break; + case PaymentChannelClaim: + constructed = new PaymentChannelClaim(); + break; + case CheckCreate: + constructed = new CheckCreate(); + break; + case CheckCash: + constructed = new CheckCash(); + break; + case CheckCancel: + constructed = new CheckCancel(); + break; + case DepositPreauth: + constructed = new DepositPreauth(); + break; + case TrustSet: + constructed = new TrustSet(); + break; + case EnableAmendment: + constructed = new EnableAmendment(); + break; + case SetFee: + constructed = new SetFee(); + break; + } + + constructed.fields = source.fields; + return constructed; + + } + + private static STObject ledgerFormatted(STObject source, LedgerEntryType ledgerEntryType) { + STObject constructed = null; + switch (ledgerEntryType) { + case Escrow: + constructed = new Escrow(); + break; + case Offer: + constructed = new Offer(); + break; + case RippleState: + constructed = new RippleState(); + break; + case AccountRoot: + constructed = new AccountRoot(); + break; + case DirectoryNode: + if (source.has(AccountID.Owner)) { + constructed = new OwnerDirectory(); + } else { + constructed = new OfferDirectory(); + } + break; + case LedgerHashes: + constructed = new LedgerHashes(); + break; + case Amendments: + constructed = new Amendments(); + break; + case FeeSettings: + constructed = new FeeSettings(); + break; + case Ticket: + constructed = new Ticket(); + break; + case SignerList: + constructed = new SignerList(); + break; + case PayChannel: + constructed = new PayChannel(); + break; + case Check: + constructed = new Check(); + break; + case DepositPreauth: + constructed = new DepositPreauthLe(); + break; + } + constructed.fields = source.fields; + return constructed; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Vector256.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Vector256.java new file mode 100644 index 0000000000..8a5eef49e5 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Vector256.java @@ -0,0 +1,141 @@ +package com.ripple.core.coretypes; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Type; +import com.ripple.core.fields.Vector256Field; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import com.ripple.encodings.common.B16; +import org.json.JSONArray; + +import java.util.ArrayList; + +public class Vector256 extends ArrayList implements SerializedType { + + @Override + public Object toJSON() { + return toJSONArray(); + } + + public JSONArray toJSONArray() { + JSONArray array = new JSONArray(); + + for (Hash256 hash256 : this) { + array.put(hash256.toString()); + } + + return array; + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + for (Hash256 hash256 : this) { + hash256.toBytesSink(to); + } + } + + @Override + public Type type() { + return Type.Vector256; + } + + /** + * This method puts the last element in the removed elements slot, and + * pops off the back, thus preserving contiguity but losing ordering. + * @param ledgerIndex the ledger entry index to remove + * + * Unused when the featureSortedDirectories amendment is applied: + * See: https://ripple.com/build/known-amendments/#sorteddirectories + */ + public boolean removeUnstable(Hash256 ledgerIndex) { + int i = indexOf(ledgerIndex); + if (i == -1) { + return false; + } + + int last = size() - 1; + Hash256 lastIndex = get(last); + set(i, lastIndex); + remove(last); + + return true; + } + + public static Vector256 fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + + public static Vector256 fromHex(String hex) { + return translate.fromHex(hex); + } + public static Vector256 fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + public static class Translator extends TypeTranslator { + @Override + public Vector256 fromParser(BinaryParser parser, Integer hint) { + Vector256 vector256 = new Vector256(); + if (hint == null) { + hint = parser.size() - parser.pos(); + } + for (int i = 0; i < hint / 32; i++) { + vector256.add(Hash256.fromParser(parser)); + } + + return vector256; + } + + public JSONArray toJSONArray(Vector256 obj) { + return obj.toJSONArray(); + } + + @Override + public Vector256 fromJSONArray(JSONArray jsonArray) { + Vector256 vector = new Vector256(); + + for (int i = 0; i < jsonArray.length(); i++) { + String hex = jsonArray.getString(i); + vector.add(new Hash256(B16.decode(hex))); + } + + return vector; + } + @Override + public Vector256 fromJacksonArray(ArrayNode jsonArray) { + Vector256 vector = new Vector256(); + + for (int i = 0; i < jsonArray.size(); i++) { + String hex = jsonArray.get(i).asText(); + vector.add(new Hash256(B16.decode(hex))); + } + + return vector; + } + } + static public Translator translate = new Translator(); + + public Vector256(){} + + public static Vector256Field vector256Field(final Field f) { + return new Vector256Field(){ @Override public Field getField() {return f;}}; + } + + static public Vector256Field Indexes = vector256Field(Field.Indexes); + static public Vector256Field Hashes = vector256Field(Field.Hashes); + static public Vector256Field Amendments = vector256Field(Field.Amendments); +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/HalfSha512.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/HalfSha512.java new file mode 100644 index 0000000000..560cce14a0 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/HalfSha512.java @@ -0,0 +1,68 @@ +package com.ripple.core.coretypes.hash; + +import com.ripple.core.coretypes.hash.prefixes.Prefix; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; + +import java.security.MessageDigest; + +public class HalfSha512 implements BytesSink { + private MessageDigest messageDigest; + + public HalfSha512() { + try { + messageDigest = MessageDigest.getInstance("SHA-512"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static HalfSha512 prefixed256(Prefix bytes) { + HalfSha512 halfSha512 = new HalfSha512(); + halfSha512.update(bytes); + return halfSha512; + } + + public void update(byte[] bytes) { + messageDigest.update(bytes); + } + + public void update(Hash256 hash) { + messageDigest.update(hash.bytes()); + } + + public MessageDigest digest() { + return messageDigest; + } + + public Hash256 finish() { + byte[] half = digestBytes(); + return new Hash256(half); + } + + private byte[] digestBytes() { + byte[] digest = messageDigest.digest(); + byte[] half = new byte[32]; + System.arraycopy(digest, 0, half, 0, 32); + return half; + } + + @Override + public void add(byte aByte) { + messageDigest.update(aByte); + } + + @Override + public void add(byte[] bytes) { + messageDigest.update(bytes); + } + + public void update(Prefix prefix) { + messageDigest.update(prefix.bytes()); + } + + public HalfSha512 add(SerializedType st) { + st.toBytesSink(this); + return this; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash.java new file mode 100644 index 0000000000..898d1b2913 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash.java @@ -0,0 +1,155 @@ +package com.ripple.core.coretypes.hash; + +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import com.ripple.encodings.common.B16; + +import java.math.BigInteger; +import java.util.Arrays; + +abstract public class Hash implements SerializedType, Comparable { + protected final byte[] hash; + + public Hash(byte[] bytes, int size) { + hash = checkHash(bytes, size); + } + + @Override + public String toString() { + return B16.encode(hash); + } + + @Override + public int hashCode() { + // Having this lazily computed and then cached would give significant + // performance boosts in some single threaded contexts. Given the inputs + // for the method are at least effectively final, and the method is + // essentially a pure function, it seems probably safe to do. It + // seems at worst, that in a multi threaded context, some extra work + // will be done in rare cases? + // TODO: check this assumption + return Arrays.hashCode(hash); + } + + private byte[] checkHash(byte[] bytes, int size) { + int length = bytes.length; + if (length > size) { + throwIllegalArg(length, "wide"); + } else if (length == size) { + // TODO: What costs for this ? + return bytes.clone(); + } else { + throwIllegalArg(length, "small"); + } + throw new IllegalStateException("Can not get here"); + } + + private void throwIllegalArg(int length, String wrongness) { + String simpleName = getClass().getSimpleName(); + throw new IllegalArgumentException("Hash length of " + length + "bytes is too " + + wrongness + + " for " + simpleName); + } + + BigInteger bigInteger() { + return new BigInteger(1, hash); + } + + // TODO: Is a defensive copy worthwhile? Or just be adults? Never been a + // problem in practice, though others may not know to avoid mutating the + // returned array. Pity Java doesn't have something like C++ const. + // If you did it in one place, should do it EVERY WHERE, inputs/outputs and + // on every type that stores mutable arrays internally and the perf and + // maybe memory (at least GC stress) costs could add up. + // It's a bit like polishing a tur*. + public byte[] bytes() { + return hash.clone(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Hash) { + return Arrays.equals(hash, ((Hash) obj).hash); + } + + return super.equals(obj); + } + + @Override + public int compareTo(Subclass another) { + byte[] thisBytes = bytes(); + byte[] bytes = another.bytes(); + + return compareBytes(thisBytes, bytes, 0, thisBytes.length); + } + + public int compareStartingAt(Subclass another, int start) { + byte[] thisBytes = bytes(); + byte[] bytes = another.bytes(); + + return compareBytes(thisBytes, bytes, start, thisBytes.length); + } + + private int compareBytes(byte[] thisBytes, byte[] bytes, int start, int numBytes) { + int thisLength = thisBytes.length; + if (!(bytes.length == thisLength)) { + throw new RuntimeException(); + } + + for (int i = start; i < numBytes; i++) { + int cmp = (thisBytes[i] & 0xFF) - (bytes[i] & 0xFF); + if (cmp != 0) { + return cmp < 0 ? -1 : 1; + } + } + return 0; + } + + public byte[] slice(int start) { + return slice(start, 0); + } + + public byte get(int i) { + if (i < 0) i += hash.length; + return hash[i]; + } + + private byte[] slice(int start, int end) { + if (start < 0) start += hash.length; + if (end <= 0) end += hash.length; + + int length = end - start; + byte[] slice = new byte[length]; + + System.arraycopy(hash, start, slice, 0, length); + return slice; + } + + static public abstract class HashTranslator extends TypeTranslator { + + public abstract T newInstance(byte[] b); + public abstract int byteWidth(); + + @Override + public T fromParser(BinaryParser parser, Integer hint) { + return newInstance(parser.read(byteWidth())); + } + + @Override + public Object toJSON(T obj) { + return B16.encode(obj.hash); + } + + @Override + public T fromString(String value) { + return newInstance(B16.decode(value)); + } + + @Override + public void toBytesSink(T obj, BytesSink to) { + to.add(obj.bytes()); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash128.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash128.java new file mode 100644 index 0000000000..112340da9f --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash128.java @@ -0,0 +1,70 @@ +package com.ripple.core.coretypes.hash; + +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Hash128Field; +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; + +public class Hash128 extends Hash { + public Hash128(byte[] bytes) { + super(bytes, 16); + } + + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + translate.toBytesSink(this, to); + } + + @Override + public Type type() { + return Type.Hash128; + } + + public static Hash128 fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + + public static Hash128 fromHex(String string) { + return translate.fromHex(string); + } + public static Hash128 fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + public static class Translator extends HashTranslator { + @Override + public Hash128 newInstance(byte[] b) { + return new Hash128(b); + } + + @Override + public int byteWidth() { + return 16; + } + } + + public static Translator translate = new Translator(); + + private static Hash128Field hash128Field(final Field f) { + return new Hash128Field(){ @Override public Field getField() {return f;}}; + } + + static public Hash128Field EmailHash = hash128Field(Field.EmailHash); + +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash160.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash160.java new file mode 100644 index 0000000000..5f237c195c --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash160.java @@ -0,0 +1,81 @@ +package com.ripple.core.coretypes.hash; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Hash160Field; +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; + +public class Hash160 extends Hash { + public Hash160(byte[] bytes) { + super(bytes, 20); + } + + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + translate.toBytesSink(this, to); + } + + @Override + public Type type() { + return Type.Hash160; + } + + public static Hash160 fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + + public static Hash160 fromHex(String string) { + return translate.fromHex(string); + } + + public static Hash160 fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + public static class Translator extends HashTranslator { + @Override + public Hash160 newInstance(byte[] b) { + return new Hash160(b); + } + + @Override + public int byteWidth() { + return 20; + } + + @Override + public Hash160 fromString(String value) { + if (value.startsWith("r")) { + return newInstance(AccountID.fromAddress(value).bytes()); + } + return super.fromString(value); + } + } + public static Translator translate = new Translator(); + + private static Hash160Field hash160Field(final Field f) { + return new Hash160Field(){ @Override public Field getField() {return f;}}; + } + + static public Hash160Field TakerPaysIssuer = hash160Field(Field.TakerPaysIssuer); + static public Hash160Field TakerGetsCurrency = hash160Field(Field.TakerGetsCurrency); + static public Hash160Field TakerPaysCurrency = hash160Field(Field.TakerPaysCurrency); + static public Hash160Field TakerGetsIssuer = hash160Field(Field.TakerGetsIssuer); +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash256.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash256.java new file mode 100644 index 0000000000..678b15ef00 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash256.java @@ -0,0 +1,146 @@ +package com.ripple.core.coretypes.hash; + +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.hash.prefixes.Prefix; +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Hash256Field; +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; + +import java.math.BigInteger; +import java.util.TreeMap; + +public class Hash256 extends Hash { + + public static final BigInteger bookBaseSize = new BigInteger("10000000000000000", 16); + + public int divergenceDepth(Hash256 other) { + return divergenceDepth(0, other); + } + public int divergenceDepth(int i, Hash256 other) { + for (; i < 64; i++) { + if (nibblet(i) != other.nibblet(i)) { + break; + } + } + return i; + } + + public static class Hash256Map extends TreeMap { + public Hash256Map(Hash256Map cache) { + super(cache); + } + public Hash256Map() { + + } + } + public static final Hash256 ZERO_256 = new Hash256(new byte[32]); + + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + translate.toBytesSink(this, to); + } + + @Override + public Type type() { + return Type.Hash256; + } + + public boolean isZero() { + return this == Hash256.ZERO_256 || equals(Hash256.ZERO_256); + } + + public boolean isNonZero() { + return !isZero(); + } + + public static Hash256 fromHex(String s) { + return translate.fromHex(s); + } + public static Hash256 fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + public static Hash256 fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + public Hash256(byte[] bytes) { + super(bytes, 32); + } + + public static Hash256 signingHash(byte[] blob) { + return prefixedHalfSha512(HashPrefix.txSign, blob); + } + + public static Hash256 prefixedHalfSha512(Prefix prefix, byte[] blob) { + HalfSha512 messageDigest = HalfSha512.prefixed256(prefix); + messageDigest.update(blob); + return messageDigest.finish(); + } + + public int nibblet(int depth) { + int byte_ix = depth > 0 ? depth / 2 : 0; + int b = super.hash[byte_ix]; + if (depth % 2 == 0) { + b = (b & 0xF0) >> 4; + } else { + b = b & 0x0F; + } + return b; + } + + public static class Translator extends HashTranslator { + @Override + public Hash256 newInstance(byte[] b) { + return new Hash256(b); + } + + @Override + public int byteWidth() { + return 32; + } + } + public static Translator translate = new Translator(); + + public static Hash256Field hash256Field(final Field f) { + return new Hash256Field(){ @Override public Field getField() {return f;}}; + } + + static public Hash256Field LedgerHash = hash256Field(Field.LedgerHash); + static public Hash256Field ParentHash = hash256Field(Field.ParentHash); + static public Hash256Field TransactionHash = hash256Field(Field.TransactionHash); + static public Hash256Field AccountHash = hash256Field(Field.AccountHash); + static public Hash256Field PreviousTxnID = hash256Field(Field.PreviousTxnID); + static public Hash256Field AccountTxnID = hash256Field(Field.AccountTxnID); + static public Hash256Field LedgerIndex = hash256Field(Field.LedgerIndex); + static public Hash256Field WalletLocator = hash256Field(Field.WalletLocator); + static public Hash256Field RootIndex = hash256Field(Field.RootIndex); + static public Hash256Field BookDirectory = hash256Field(Field.BookDirectory); + static public Hash256Field InvoiceID = hash256Field(Field.InvoiceID); + static public Hash256Field Nickname = hash256Field(Field.Nickname); + static public Hash256Field Amendment = hash256Field(Field.Amendment); + static public Hash256Field TicketID = hash256Field(Field.TicketID); + static public Hash256Field Channel = hash256Field(Field.Channel); + static public Hash256Field CheckID = hash256Field(Field.CheckID); + static public Hash256Field Digest = hash256Field(Field.Digest); + static public Hash256Field ConsensusHash = hash256Field(Field.ConsensusHash); + + static public Hash256Field hash = hash256Field(Field.hash); + static public Hash256Field index = hash256Field(Field.index); +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Index.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Index.java new file mode 100644 index 0000000000..75a5900430 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Index.java @@ -0,0 +1,159 @@ +package com.ripple.core.coretypes.hash; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Currency; +import com.ripple.core.coretypes.Issue; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.hash.prefixes.LedgerSpace; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; + +import java.util.Arrays; +import java.util.List; + +import static com.ripple.core.coretypes.hash.HalfSha512.prefixed256; +import static java.util.Collections.sort; + +public class Index { + private static Hash256 createBookBase(Issue pays, Issue gets) { + return prefixed256(LedgerSpace.bookDir) + .add(pays.currency()) + .add(gets.currency()) + .add(pays.issuer()) + .add(gets.issuer()) + .finish(); + } + + /** + * + * @return a copy of index, with quality overlaid in lowest 8 bytes + */ + public static Hash256 quality(Hash256 index, UInt64 quality) { + byte[] qi = new byte[32]; + System.arraycopy(index.bytes(), 0, qi, 0, 24); + if (quality != null) System.arraycopy(quality.toBytes(), 0, qi, 24, 8); + return new Hash256(qi); + } + + /** + * @return A copy of index, with the lowest 8 bytes all zeroed. + */ + private static Hash256 zeroQuality(Hash256 fullIndex) { + return quality(fullIndex, null); + } + + public static Hash256 rippleState(AccountID a1, AccountID a2, Currency currency) { + List accounts = Arrays.asList(a1, a2); + sort(accounts); + return rippleState(accounts, currency); + } + + public static Hash256 rippleState(List sortedAccounts, Currency currency) { + HalfSha512 hasher = prefixed256(LedgerSpace.ripple); + // Low then High + for (AccountID account : sortedAccounts) account.toBytesSink(hasher); + // Currency + currency.toBytesSink(hasher); + + return hasher.finish(); + } + + /** + * + * @param rootIndex The RootIndex index for the directory node + * @param nodeIndex nullable LowNode, HighNode, OwnerNode, BookNode etc + * defining a `page` number. + * + * @return A hash of rootIndex and nodeIndex when nodeIndex is non default + * else the rootIndex. This hash is used as an index for the next + * DirectoryNode page. + */ + public static Hash256 directoryNode(Hash256 rootIndex, UInt64 nodeIndex) { + if (nodeIndex == null || nodeIndex.isZero()) { + return rootIndex; + } + + return prefixed256(LedgerSpace.dirNode) + .add(rootIndex) + .add(nodeIndex) + .finish(); + } + + public static Hash256 accountRoot(AccountID accountID) { + return prefixed256(LedgerSpace.account).add(accountID).finish(); + } + + public static Hash256 paymentChannel(AccountID account, AccountID destination, UInt32 sequence) { + return prefixed256(LedgerSpace.paymentChannel) + .add(account) + .add(destination) + .add(sequence) + .finish(); + } + + public static Hash256 escrow(AccountID account, UInt32 sequence) { + return prefixed256(LedgerSpace.escrow) + .add(account) + .add(sequence) + .finish(); + } + + public static Hash256 ownerDirectory(AccountID account) { + return Hash256.prefixedHalfSha512(LedgerSpace.ownerDir, account.bytes()); + } + + public static Hash256 transactionID(byte[] blob) { + return Hash256.prefixedHalfSha512(HashPrefix.transactionID, blob); + } + + public static Hash256 bookStart(Issue pays, Issue gets) { + return zeroQuality(createBookBase(pays, gets)); + } + + public static Hash256 bookStart(Hash256 indexFromBookRange) { + return zeroQuality(indexFromBookRange); + } + + public static Hash256 bookEnd(Hash256 base) { + byte[] end = base.bigInteger().add(Hash256.bookBaseSize).toByteArray(); + if (end.length > 32) { + byte[] source = end; + end = new byte[32]; + System.arraycopy(source, source.length - 32, end, 0, 32); + } + return new Hash256(end); + } + + public static Hash256 ledgerHashes(long prev) { + return prefixed256(LedgerSpace.skipList) + .add(new UInt32(prev >> 16)) + .finish(); + } + public static Hash256 ledgerHashes() { + return prefixed256(LedgerSpace.skipList).finish(); + } + + public static Hash256 amendments() { + return prefixed256(LedgerSpace.amendment).finish(); + } + + public static Hash256 feeSettings() { + return prefixed256(LedgerSpace.fee).finish(); + } + + public static Hash256 signerList(AccountID account) { + return prefixed256(LedgerSpace.signerList) + .add(account) + .add(UInt32.ZERO) + .finish(); + } + + public static Hash256 ticket(AccountID account, UInt32 sequence) { + return prefixed256(LedgerSpace.ticket) + .add(account) + .add(sequence) + .finish(); + } + + +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/HashPrefix.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/HashPrefix.java new file mode 100644 index 0000000000..f213d4eb38 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/HashPrefix.java @@ -0,0 +1,56 @@ +package com.ripple.core.coretypes.hash.prefixes; + +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.encodings.common.B16; + +/** + * The prefix codes are part of the Ripple protocol + * and existing codes cannot be arbitrarily changed. + */ +public enum HashPrefix implements Prefix { + transactionID ('T', 'X', 'N'), + txNode ('S', 'N', 'D'), + leafNode ('M', 'L', 'N'), + innerNode ('M', 'I', 'N'), + innerNodeV2 ('I', 'N', 'R'), + ledgerMaster ('L', 'W', 'R'), + txSign ('S', 'T', 'X'), + txMultiSign ('S', 'M', 'T'), + validation ('V', 'A', 'L'), + proposal ('P', 'R', 'P'), + manifest ('M', 'A', 'N'), + paymentChannelClaim ('C', 'L', 'M'); + + private UInt32 uInt32; + private byte[] bytes; + private String chars; + + @Override + public byte[] bytes() { + return bytes; + } + + public String toHex() { + return B16.encode(bytes); + } + + HashPrefix(char... chars) { + this.chars = new String(chars); + byte[] bytes = { + (byte) chars[0], + (byte) chars[1], + (byte) chars[2], + (byte) 0, + }; + uInt32 = UInt32.fromBytes(bytes); + this.bytes = bytes; + } + + public UInt32 uInt32() { + return uInt32; + } + + public String chars() { + return chars; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/LedgerSpace.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/LedgerSpace.java new file mode 100644 index 0000000000..20f5d61388 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/LedgerSpace.java @@ -0,0 +1,38 @@ +package com.ripple.core.coretypes.hash.prefixes; + +import com.ripple.core.coretypes.uint.UInt16; + +public enum LedgerSpace implements Prefix { + account('a'), + dirNode('d'), + generator('g'), + + ripple('r'), + offer('o'), // Entry for an offer. + ownerDir('O'), // Directory of things owned by an account. + bookDir('B'), // Directory of order books. + contract('c'), + skipList('s'), + escrow('u'), + amendment('f'), + fee('e'), + ticket('T'), + signerList('S'), + paymentChannel('x'), + + // no longer used + nickname('n'),; + + UInt16 uInt16; + public byte[] bytes; + + @Override + public byte[] bytes() { + return bytes; + } + + LedgerSpace(char c) { + uInt16 = new UInt16((int) c); + bytes = uInt16.toByteArray(); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/Prefix.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/Prefix.java new file mode 100644 index 0000000000..7ba8b7a62a --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/Prefix.java @@ -0,0 +1,5 @@ +package com.ripple.core.coretypes.hash.prefixes; + +public interface Prefix { + byte[] bytes(); +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt.java new file mode 100644 index 0000000000..aae0dcedf3 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt.java @@ -0,0 +1,209 @@ +package com.ripple.core.coretypes.uint; + +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import com.ripple.encodings.common.B16; +import com.ripple.utils.Utils; + +import java.math.BigInteger; + +abstract public class UInt extends Number implements SerializedType, Comparable { + private final BigInteger value; + + private static BigInteger[] upperBounds = new BigInteger[8]; + private static BigInteger maxUIntVal(int bits) { + return new BigInteger("2").pow(bits).subtract(BigInteger.ONE); + } + + static { + for (int i = 0; i < 8; i++) { + upperBounds[i] = maxUIntVal((i + 1) * 8 ); + } + } + + UInt(byte[] bytes) { + value = new BigInteger(1, bytes); + checkBounds(); + } + + private void checkBounds() { + BigInteger upper = upperBounds[getByteWidth() - 1]; + if (value.compareTo(upper) > 0 || value.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException("value `" + value + + "` is illegal for " + getClass().getSimpleName()); + } + } + + UInt(BigInteger bi) { + value = (bi); + checkBounds(); + } + UInt(Number s) { + value = (BigInteger.valueOf(s.longValue())); + checkBounds(); + } + UInt(String s) { + value = (new BigInteger(s)); + checkBounds(); + } + UInt(String s, int radix) { + value = (new BigInteger(s, radix)); + checkBounds(); + } + + @Override + public String toString() { + return value.toString(); + } + + public abstract int getByteWidth(); + protected abstract Subclass instanceFrom(BigInteger n); + + public Subclass add(UInt val) { + return instanceFrom(value.add(val.value)); + } + + public Subclass subtract(UInt val) { + return instanceFrom(value.subtract(val.value)); + } + + public Subclass multiply(UInt val) { + return instanceFrom(value.multiply(val.value)); + } + + public Subclass divide(UInt val) { + return instanceFrom(value.divide(val.value)); + } + + public Subclass or(UInt val) { + return instanceFrom(value.or(val.value)); + } + + public Subclass shiftLeft(int n) { + return instanceFrom(value.shiftLeft(n)); + } + + public Subclass shiftRight(int n) { + return instanceFrom(value.shiftRight(n)); + } + + public int compareTo(UInt val) { + return value.compareTo(val.value); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof UInt) { + return equals((UInt) obj); + } + else return super.equals(obj); + } + + public boolean equals(UInt x) { + return value.equals(x.value); + } + + public String toString(int radix) { + return value.toString(radix); + } + public byte[] toByteArray() { + int length = getByteWidth(); + return Utils.leadingZeroesTrimmedOrPaddedTo(length, value.toByteArray()); + } + + abstract public Object value(); + + public BigInteger bigInteger(){ + return value; + } + + @Override + public int intValue() { + return value.intValue(); + } + + @Override + public long longValue() { + return value.longValue(); + } + + @Override + public double doubleValue() { + return value.doubleValue(); + } + + @Override + public float floatValue() { + return value.floatValue(); + } + + @Override + public byte byteValue() { + return value.byteValue(); + } + + @Override + public short shortValue() { + return value.shortValue(); + } + + public boolean lte(T sequence) { + return compareTo(sequence) < 1; + } + + public boolean testBit(int f) { + // TODO, optimized ;) // move to Uint32 + return value.testBit(f); + } + + public boolean isZero() { + return value.signum() == 0; + } + + static public abstract class UINTTranslator extends TypeTranslator { + public abstract T newInstance(BigInteger i); + public abstract int byteWidth(); + + @Override + public T fromParser(BinaryParser parser, Integer hint) { + return newInstance(new BigInteger(1, parser.read(byteWidth()))); + } + + @Override + public Object toJSON(T obj) { + if (obj.getByteWidth() <= 4) { + return obj.longValue(); + } else { + return toString(obj); + } + } + + @Override + public T fromLong(long aLong) { + return newInstance(BigInteger.valueOf(aLong)); + } + + @Override + public T fromString(String value) { + int radix = byteWidth() <= 4 ? 10 : 16; + return newInstance(new BigInteger(value, radix)); + } + + @Override + public T fromInteger(int integer) { + return fromLong(integer); + } + + @Override + public String toString(T obj) { + return B16.encode(obj.toByteArray()); + } + + @Override + public void toBytesSink(T obj, BytesSink to) { + to.add(obj.toByteArray()); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt16.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt16.java new file mode 100644 index 0000000000..9634cf6082 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt16.java @@ -0,0 +1,106 @@ +package com.ripple.core.coretypes.uint; + +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Type; +import com.ripple.core.fields.UInt16Field; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.TypeTranslator; + +import java.math.BigInteger; + +public class UInt16 extends UInt { + public final static UInt16 ZERO = new UInt16(0); + + public static TypeTranslator translate = new UINTTranslator() { + @Override + public UInt16 newInstance(BigInteger i) { + return new UInt16(i); + } + + @Override + public int byteWidth() { + return 2; + } + }; + + public UInt16(byte[] bytes) { + super(bytes); + } + + public UInt16(BigInteger value) { + super(value); + } + + public UInt16(Number s) { + super(s); + } + + public UInt16(String s) { + super(s); + } + + public UInt16(String s, int radix) { + super(s, radix); + } + + public static UInt16 fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + + public static UInt16 fromHex(String string) { + return translate.fromHex(string); + } + + public static UInt16 fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + @Override + public int getByteWidth() { + return 2; + } + + @Override + protected UInt16 instanceFrom(BigInteger n) { + return new UInt16(n); + } + + @Override + public Integer value() { + return intValue(); + } + + private static UInt16Field int16Field(final Field f) { + return new UInt16Field(){ @Override public Field getField() {return f;}}; + } + + static public UInt16Field LedgerEntryType = int16Field(Field.LedgerEntryType); + static public UInt16Field TransactionType = int16Field(Field.TransactionType); + static public UInt16Field SignerWeight = int16Field(Field.SignerWeight); + + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + translate.toBytesSink(this, to); + } + + @Override + public Type type() { + return Type.UInt16; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt32.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt32.java new file mode 100644 index 0000000000..b831c8607c --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt32.java @@ -0,0 +1,137 @@ +package com.ripple.core.coretypes.uint; + +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Type; +import com.ripple.core.fields.UInt32Field; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.TypeTranslator; + +import java.math.BigInteger; + +public class UInt32 extends UInt { + public final static UInt32 ZERO = new UInt32(0); + + public static TypeTranslator translate = new UINTTranslator() { + @Override + public UInt32 newInstance(BigInteger i) { + return new UInt32(i); + } + + @Override + public int byteWidth() { + return 4; + } + }; + + public UInt32(byte[] bytes) { + super(bytes); + } + + public UInt32(BigInteger value) { + super(value); + } + + public UInt32(Number val) { + super(val); + } + public UInt32(String s) { + super(s); + } + + public UInt32(String s, int radix) { + super(s, radix); + } + + public static UInt32 fromParser(BinaryParser binaryParser) { + return translate.fromParser(binaryParser); + } + public static UInt32 fromHex(String string) { + return translate.fromHex(string); + } + public static UInt32 fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + @Override + public int getByteWidth() { + return 4; + } + + @Override + protected UInt32 instanceFrom(BigInteger n) { + return new UInt32(n); + } + + @Override + public Long value() { + return longValue(); + } + + private static UInt32Field int32Field(final Field f) { + return new UInt32Field(){ @Override public Field getField() {return f;}}; + } + + static public UInt32Field Flags = int32Field(Field.Flags); + static public UInt32Field SourceTag = int32Field(Field.SourceTag); + static public UInt32Field Sequence = int32Field(Field.Sequence); + static public UInt32Field PreviousTxnLgrSeq = int32Field(Field.PreviousTxnLgrSeq); + static public UInt32Field LedgerSequence = int32Field(Field.LedgerSequence); + static public UInt32Field CloseTime = int32Field(Field.CloseTime); + static public UInt32Field ParentCloseTime = int32Field(Field.ParentCloseTime); + static public UInt32Field SigningTime = int32Field(Field.SigningTime); + static public UInt32Field Expiration = int32Field(Field.Expiration); + static public UInt32Field TransferRate = int32Field(Field.TransferRate); + static public UInt32Field WalletSize = int32Field(Field.WalletSize); + static public UInt32Field OwnerCount = int32Field(Field.OwnerCount); + static public UInt32Field DestinationTag = int32Field(Field.DestinationTag); + static public UInt32Field HighQualityIn = int32Field(Field.HighQualityIn); + static public UInt32Field HighQualityOut = int32Field(Field.HighQualityOut); + static public UInt32Field LowQualityIn = int32Field(Field.LowQualityIn); + static public UInt32Field LowQualityOut = int32Field(Field.LowQualityOut); + static public UInt32Field QualityIn = int32Field(Field.QualityIn); + static public UInt32Field QualityOut = int32Field(Field.QualityOut); + static public UInt32Field StampEscrow = int32Field(Field.StampEscrow); + static public UInt32Field BondAmount = int32Field(Field.BondAmount); + static public UInt32Field LoadFee = int32Field(Field.LoadFee); + static public UInt32Field OfferSequence = int32Field(Field.OfferSequence); + static public UInt32Field FirstLedgerSequence = int32Field(Field.FirstLedgerSequence); + static public UInt32Field LastLedgerSequence = int32Field(Field.LastLedgerSequence); + static public UInt32Field TransactionIndex = int32Field(Field.TransactionIndex); + static public UInt32Field OperationLimit = int32Field(Field.OperationLimit); + static public UInt32Field ReferenceFeeUnits = int32Field(Field.ReferenceFeeUnits); + static public UInt32Field ReserveBase = int32Field(Field.ReserveBase); + static public UInt32Field ReserveIncrement = int32Field(Field.ReserveIncrement); + static public UInt32Field SetFlag = int32Field(Field.SetFlag); + static public UInt32Field ClearFlag = int32Field(Field.ClearFlag); + static public UInt32Field SignerQuorum = int32Field(Field.SignerQuorum); + public static UInt32Field SignerListID = int32Field(Field.SignerListID); + public static UInt32Field CancelAfter = int32Field(Field.CancelAfter); + public static UInt32Field FinishAfter = int32Field(Field.FinishAfter); + public static UInt32Field SettleDelay = int32Field(Field.SettleDelay); + + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + translate.toBytesSink(this, to); + } + + @Override + public Type type() { + return Type.UInt32; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt64.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt64.java new file mode 100644 index 0000000000..3312192542 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt64.java @@ -0,0 +1,112 @@ +package com.ripple.core.coretypes.uint; + +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Type; +import com.ripple.core.fields.UInt64Field; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.TypeTranslator; + +import java.math.BigInteger; + +public class UInt64 extends UInt { + public final static UInt64 ZERO = new UInt64(0); + + public static TypeTranslator translate = new UINTTranslator() { + @Override + public UInt64 newInstance(BigInteger i) { + return new UInt64(i); + } + + @Override + public int byteWidth() { + return 8; + } + }; + + public UInt64(byte[] bytes) { + super(bytes); + } + + public UInt64(BigInteger value) { + super(value); + } + + public UInt64(Number s) { + super(s); + } + + public UInt64(String s) { + super(s); + } + + public UInt64(String s, int radix) { + super(s, radix); + } + + public static UInt64 fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + + public static UInt64 fromHex(String string) { + return translate.fromHex(string); + } + public static UInt64 fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + @Override + public int getByteWidth() { + return 8; + } + + @Override + protected UInt64 instanceFrom(BigInteger n) { + return new UInt64(n); + } + + @Override + public BigInteger value() { + return bigInteger(); + } + + private static UInt64Field int64Field(final Field f) { + return new UInt64Field(){ @Override public Field getField() {return f;}}; + } + + static public UInt64Field IndexNext = int64Field(Field.IndexNext); + static public UInt64Field IndexPrevious = int64Field(Field.IndexPrevious); + static public UInt64Field BookNode = int64Field(Field.BookNode); + static public UInt64Field OwnerNode = int64Field(Field.OwnerNode); + static public UInt64Field BaseFee = int64Field(Field.BaseFee); + static public UInt64Field ExchangeRate = int64Field(Field.ExchangeRate); + static public UInt64Field LowNode = int64Field(Field.LowNode); + static public UInt64Field HighNode = int64Field(Field.HighNode); + public static UInt64Field DestinationNode = int64Field(Field.DestinationNode); + public static UInt64Field Cookie = int64Field(Field.Cookie); + + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + translate.toBytesSink(this, to); + } + + @Override + public Type type() { + return Type.UInt64; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt8.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt8.java new file mode 100644 index 0000000000..528eaa8614 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt8.java @@ -0,0 +1,105 @@ +package com.ripple.core.coretypes.uint; + +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Type; +import com.ripple.core.fields.UInt8Field; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.TypeTranslator; + +import java.math.BigInteger; + +public class UInt8 extends UInt { + public final static UInt8 ZERO = new UInt8(0); + + public static TypeTranslator translate = new UINTTranslator() { + @Override + public UInt8 newInstance(BigInteger i) { + return new UInt8(i); + } + + @Override + public int byteWidth() { + return 1; + } + }; + + public UInt8(byte[] bytes) { + super(bytes); + } + + public UInt8(BigInteger value) { + super(value); + } + + public UInt8(Number s) { + super(s); + } + + public UInt8(String s) { + super(s); + } + + public UInt8(String s, int radix) { + super(s, radix); + } + + public static UInt8 fromParser(BinaryParser parser) { + return translate.fromParser(parser); + } + public static UInt8 fromHex(String hex) { + return translate.fromHex(hex); + } + public static UInt8 fromBytes(byte[] bytes) { + return translate.fromBytes(bytes); + } + + @Override + public int getByteWidth() { + return 1; + } + + @Override + protected UInt8 instanceFrom(BigInteger n) { + return new UInt8(n); + } + + @Override + public Short value() { + return shortValue(); + } + + private static UInt8Field int8Field(final Field f) { + return new UInt8Field() {@Override public Field getField() {return f; } }; + } + + static public UInt8Field CloseResolution = int8Field(Field.CloseResolution); + static public UInt8Field Method = int8Field(Field.Method); + static public UInt8Field TransactionResult = int8Field(Field.TransactionResult); + static public UInt8Field TickSize = int8Field(Field.TickSize); + + @Override + public Object toJSON() { + return translate.toJSON(this); + } + + @Override + public byte[] toBytes() { + return translate.toBytes(this); + } + + @Override + public String toHex() { + return translate.toHex(this); + } + + @Override + public void toBytesSink(BytesSink to) { + translate.toBytesSink(this, to); + } + + @Override + public Type type() { + return Type.UInt8; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/enums/LedgerFlag.java b/ripple-core/src/main/java/com/ripple/core/enums/LedgerFlag.java new file mode 100644 index 0000000000..99f18b5309 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/enums/LedgerFlag.java @@ -0,0 +1,29 @@ +package com.ripple.core.enums; + +// Ledger Specific Flags +public class LedgerFlag { + public static int + // ltACCOUNT_ROOT + PasswordSpent = 0x00010000, // True, if password set fee is spent. + RequireDestTag = 0x00020000, // True, to require a DestinationTag for payments. + RequireAuth = 0x00040000, // True, to require a authorization to hold IOUs. + DisallowXRP = 0x00080000, // True, to disallow sending XRP. + DisableMaster = 0x00100000, // True, force regular key + NoFreeze = 0x00200000, // True, cannot freeze ripple states + GlobalFreeze = 0x00400000, // True, all assets frozen + DefaultRipple = 0x00800000, // True, all assets frozen + + // ltOFFER + Passive = 0x00010000, + Sell = 0x00020000, // True, offer was placed as a sell. + + // ltRIPPLE_STATE + LowReserve = 0x00010000, // True, if entry counts toward reserve. + HighReserve = 0x00020000, + LowAuth = 0x00040000, + HighAuth = 0x00080000, + LowNoRipple = 0x00100000, + HighNoRipple = 0x00200000, + LowFreeze = 0x00400000, // True, low side has set freeze flag + HighFreeze = 0x00800000; // True, high side has set freeze flag +} diff --git a/ripple-core/src/main/java/com/ripple/core/enums/TransactionFlag.java b/ripple-core/src/main/java/com/ripple/core/enums/TransactionFlag.java new file mode 100644 index 0000000000..adb7f9e910 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/enums/TransactionFlag.java @@ -0,0 +1,50 @@ +package com.ripple.core.enums; + +// Transaction Specific Flags +public class TransactionFlag { + public static long + FullyCanonicalSig = 0x80000000L, + Universal = FullyCanonicalSig, + UniversalMask = ~Universal, + + // AccountSet flags: + RequireDestTag = 0x00010000, + OptionalDestTag = 0x00020000, + RequireAuth = 0x00040000, + OptionalAuth = 0x00080000, + DisallowXRP = 0x00100000, + AllowXRP = 0x00200000, + AccountSetMask = ~(Universal | RequireDestTag | OptionalDestTag + | RequireAuth | OptionalAuth + | DisallowXRP | AllowXRP), + + // AccountSet SetFlag/ClearFlag values + asfRequireDest = 1, + asfRequireAuth = 2, + asfDisallowXRP = 3, + asfDisableMaster = 4, + asfAccountTxnID = 5, + asfNoFreeze = 6, + asfGlobalFreeze = 7, + + // OfferCreate flags: + Passive = 0x00010000, + ImmediateOrCancel = 0x00020000, + FillOrKill = 0x00040000, + Sell = 0x00080000, + OfferCreateMask = ~(Universal | Passive | ImmediateOrCancel | FillOrKill | Sell), + + // Payment flags: + NoRippleDirect = 0x00010000, + PartialPayment = 0x00020000, + LimitQuality = 0x00040000, + PaymentMask = ~(Universal | PartialPayment | LimitQuality | NoRippleDirect), + + // TrustSet flags: + SetAuth = 0x00010000, + SetNoRipple = 0x00020000, + ClearNoRipple = 0x00040000, + SetFreeze = 0x00100000, + ClearFreeze = 0x00200000, + TrustSetMask = ~(Universal | SetAuth | SetNoRipple | ClearNoRipple | SetFreeze | ClearFreeze); +} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/AccountIDField.java b/ripple-core/src/main/java/com/ripple/core/fields/AccountIDField.java new file mode 100644 index 0000000000..f31e3df14d --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/AccountIDField.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class AccountIDField implements HasField {} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/AmountField.java b/ripple-core/src/main/java/com/ripple/core/fields/AmountField.java new file mode 100644 index 0000000000..4666ff51f9 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/AmountField.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class AmountField implements HasField {} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/BlobField.java b/ripple-core/src/main/java/com/ripple/core/fields/BlobField.java new file mode 100644 index 0000000000..2925c89611 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/BlobField.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class BlobField implements HasField{} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Field.java new file mode 100644 index 0000000000..87871ecca0 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/Field.java @@ -0,0 +1,328 @@ +package com.ripple.core.fields; + +import java.util.*; + +public enum Field { + // These are all presorted (verified in a static block below) + // They can then be used in a TreeMap, using the Enum (private) ordinal + // comparator + Generic(0, Type.Unknown), + Invalid(-1, Type.Unknown), + + LedgerEntryType(1, Type.UInt16), + TransactionType(2, Type.UInt16), + SignerWeight(3, Type.UInt16), + + Flags(2, Type.UInt32), + SourceTag(3, Type.UInt32), + Sequence(4, Type.UInt32), + PreviousTxnLgrSeq(5, Type.UInt32), + LedgerSequence(6, Type.UInt32), + CloseTime(7, Type.UInt32), + ParentCloseTime(8, Type.UInt32), + SigningTime(9, Type.UInt32), + Expiration(10, Type.UInt32), + TransferRate(11, Type.UInt32), + WalletSize(12, Type.UInt32), + OwnerCount(13, Type.UInt32), + DestinationTag(14, Type.UInt32), + + HighQualityIn(16, Type.UInt32), + HighQualityOut(17, Type.UInt32), + LowQualityIn(18, Type.UInt32), + LowQualityOut(19, Type.UInt32), + QualityIn(20, Type.UInt32), + QualityOut(21, Type.UInt32), + StampEscrow(22, Type.UInt32), + BondAmount(23, Type.UInt32), + LoadFee(24, Type.UInt32), + OfferSequence(25, Type.UInt32), + FirstLedgerSequence(26, Type.UInt32), // Deprecated: do not use + // Added new semantics in 9486fc416ca7c59b8930b734266eed4d5b714c50 + LastLedgerSequence(27, Type.UInt32), + TransactionIndex(28, Type.UInt32), + OperationLimit(29, Type.UInt32), + ReferenceFeeUnits(30, Type.UInt32), + ReserveBase(31, Type.UInt32), + ReserveIncrement(32, Type.UInt32), + SetFlag(33, Type.UInt32), + ClearFlag(34, Type.UInt32), + SignerQuorum(35, Type.UInt32), + CancelAfter(36, Type.UInt32), + FinishAfter(37, Type.UInt32), + SignerListID(38, Type.UInt32), + SettleDelay(39, Type.UInt32), + + IndexNext(1, Type.UInt64), + IndexPrevious(2, Type.UInt64), + BookNode(3, Type.UInt64), + OwnerNode(4, Type.UInt64), + BaseFee(5, Type.UInt64), + ExchangeRate(6, Type.UInt64), + LowNode(7, Type.UInt64), + HighNode(8, Type.UInt64), + DestinationNode(9, Type.UInt64), + Cookie(10, Type.UInt64), + + EmailHash(1, Type.Hash128), + + LedgerHash(1, Type.Hash256), + ParentHash(2, Type.Hash256), + TransactionHash(3, Type.Hash256), + AccountHash(4, Type.Hash256), + PreviousTxnID(5, Type.Hash256), + LedgerIndex(6, Type.Hash256), + WalletLocator(7, Type.Hash256), + RootIndex(8, Type.Hash256), + // Added in rippled commit: 9486fc416ca7c59b8930b734266eed4d5b714c50 + AccountTxnID(9, Type.Hash256), + BookDirectory(16, Type.Hash256), + InvoiceID(17, Type.Hash256), + Nickname(18, Type.Hash256), + Amendment(19, Type.Hash256), + TicketID(20, Type.Hash256), + Digest(21, Type.Hash256), + Channel(22, Type.Hash256), + ConsensusHash(23, Type.Hash256), + CheckID(24, Type.Hash256), + + hash(257, Type.Hash256), + index(258, Type.Hash256), + + Amount(1, Type.Amount), + Balance(2, Type.Amount), + LimitAmount(3, Type.Amount), + TakerPays(4, Type.Amount), + TakerGets(5, Type.Amount), + LowLimit(6, Type.Amount), + HighLimit(7, Type.Amount), + Fee(8, Type.Amount), + SendMax(9, Type.Amount), + DeliverMin(10, Type.Amount), + + MinimumOffer(16, Type.Amount), + RippleEscrow(17, Type.Amount), + // Added in rippled commit: e7f0b8eca69dd47419eee7b82c8716b3aa5a9e39 + DeliveredAmount(18, Type.Amount), + // These are auxiliary fields +// quality(257, Type.AMOUNT), + taker_gets_funded(258, Type.Amount), + taker_pays_funded(259, Type.Amount), + + PublicKey(1, Type.Blob), + MessageKey(2, Type.Blob), + SigningPubKey(3, Type.Blob), + TxnSignature(4, Type.Blob), + Signature(6, Type.Blob), + Domain(7, Type.Blob), + FundCode(8, Type.Blob), + RemoveCode(9, Type.Blob), + ExpireCode(10, Type.Blob), + CreateCode(11, Type.Blob), + MemoType(12, Type.Blob), + MemoData(13, Type.Blob), + MemoFormat(14, Type.Blob), + Fulfillment(16, Type.Blob), + Condition(17, Type.Blob), + MasterSignature(18, Type.Blob), + + Account(1, Type.AccountID), + Owner(2, Type.AccountID), + Destination(3, Type.AccountID), + Issuer(4, Type.AccountID), + Authorize(5, Type.AccountID), + Unauthorize(6, Type.AccountID), + Target(7, Type.AccountID), + RegularKey(8, Type.AccountID), + + ObjectEndMarker(1, Type.STObject), + + TransactionMetaData(2, Type.STObject), + CreatedNode(3, Type.STObject), + DeletedNode(4, Type.STObject), + ModifiedNode(5, Type.STObject), + PreviousFields(6, Type.STObject), + FinalFields(7, Type.STObject), + NewFields(8, Type.STObject), + TemplateEntry(9, Type.STObject), + Memo(10, Type.STObject), + SignerEntry(11, Type.STObject), + Signer(16, Type.STObject), + // 17 unused + Majority(18, Type.STObject), + + ArrayEndMarker(1, Type.STArray), +// SigningAccounts(2, Type.STArray), + Signers(3, Type.STArray), + SignerEntries(4, Type.STArray), + Template(5, Type.STArray), + Necessary(6, Type.STArray), + Sufficient(7, Type.STArray), + AffectedNodes(8, Type.STArray), + Memos(9, Type.STArray), + Majorities(16, Type.STArray), + + CloseResolution(1, Type.UInt8), + Method(2, Type.UInt8), + TransactionResult(3, Type.UInt8), + TickSize(16, Type.UInt8), + + TakerPaysCurrency(1, Type.Hash160), + TakerPaysIssuer(2, Type.Hash160), + TakerGetsCurrency(3, Type.Hash160), + TakerGetsIssuer(4, Type.Hash160), + + Paths(1, Type.PathSet), + + Indexes(1, Type.Vector256), + Hashes(2, Type.Vector256), + Amendments(3, Type.Vector256), + + Transaction(1, Type.Transaction), + LedgerEntry(1, Type.LedgerEntry), + Validation(1, Type.Validation); + + final int id; + + // defaults + boolean signingField = true; + boolean isSerialized = true; + boolean isVlEncoded = false; + + public static Field fromString(String key) { + Field f; + try { + f = valueOf(key); + } catch (IllegalArgumentException e) { + f = null; + } + return f; + } + + private static byte[] asBytes(Field field) { + int name = field.getId(), type = field.getType().getId(); + ArrayList header = new ArrayList<>(3); + + if (type < 16) + { + if (name < 16) // common type, common name + header.add((byte)((type << 4) | name)); + else + { + // common type, uncommon name + header.add((byte)(type << 4)); + header.add((byte)(name)); + } + } + else if (name < 16) + { + // uncommon type, common name + header.add((byte)(name)); + header.add((byte)(type)); + } + else + { + // uncommon type, uncommon name + header.add((byte)(0)); + header.add((byte)(type)); + header.add((byte)(name)); + } + + byte[] headerBytes = new byte[header.size()]; + for (int i = 0; i < header.size(); i++) { + headerBytes[i] = header.get(i); + } + + return headerBytes; + } + + public int getId() { + return id; + } + + final int code; + final Type type; + private final byte[] bytes; + public Object tag = null; + + Field(int fid, Type tid) { + id = fid; + type = tid; + code = (type.id << 16) | fid; + isSerialized = isSerialized(this); + + if (isSerialized()) { + bytes = asBytes(this); + } else { + bytes = null; + } + } + + static private Map byCode = new TreeMap<>(); + + static public Field fromCode(Integer integer) { + return byCode.get(integer); + } + + public Type getType() { + return type; + } + + public boolean isSerialized() { + return isSerialized; + } + public boolean isVLEncoded() { + return isVlEncoded; + } + public boolean isSigningField() { + return signingField; + } + private static boolean isSerialized(Field f) { + // This should screen out `hash` and `index` and the like + return ((f.type.id > 0) && (f.type.id < 256) && (f.id > 0) && (f.id < 256)); + } + + static public Comparator comparator = Comparator.comparingInt(o -> o.code); + + static { + for (Field f : Field.values()) { + byCode.put(f.code, f); + f.isSerialized = isSerialized(f); + f.signingField = f.isSerialized; + + switch (f.type) { + case Blob: + case AccountID: + case Vector256: + f.isVlEncoded = true; + break; + default: + break; + } + + } + + TxnSignature.signingField = false; + Signers.signingField = false; + Signature.signingField = false; + MasterSignature.signingField = false; + + ArrayList sortedFields; + Field[] values = Field.values(); + sortedFields = new ArrayList<>(Arrays.asList(values)); + sortedFields.sort(comparator); + + for (int i = 0; i < values.length; i++) { + Field av = values[i]; + Field lv = sortedFields.get(i); + if (av.code != lv.code) { + throw new AssertionError( + "Field enum declaration isn't presorted"); + } + } + } + + public byte[] getBytes() { + return bytes; + } +} \ No newline at end of file diff --git a/ripple-core/src/main/java/com/ripple/core/fields/HasField.java b/ripple-core/src/main/java/com/ripple/core/fields/HasField.java new file mode 100644 index 0000000000..edc49029a3 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/HasField.java @@ -0,0 +1,5 @@ +package com.ripple.core.fields; + +public interface HasField { + Field getField(); +} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Hash128Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Hash128Field.java new file mode 100644 index 0000000000..b8b78e912e --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/Hash128Field.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class Hash128Field implements HasField {} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Hash160Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Hash160Field.java new file mode 100644 index 0000000000..f0569b13eb --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/Hash160Field.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class Hash160Field implements HasField {} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Hash256Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Hash256Field.java new file mode 100644 index 0000000000..e0a83b67ef --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/Hash256Field.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class Hash256Field implements HasField {} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/PathSetField.java b/ripple-core/src/main/java/com/ripple/core/fields/PathSetField.java new file mode 100644 index 0000000000..5982570eb6 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/PathSetField.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class PathSetField implements HasField{} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/STArrayField.java b/ripple-core/src/main/java/com/ripple/core/fields/STArrayField.java new file mode 100644 index 0000000000..056a55c861 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/STArrayField.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class STArrayField implements HasField{} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/STObjectField.java b/ripple-core/src/main/java/com/ripple/core/fields/STObjectField.java new file mode 100644 index 0000000000..c802f57a2a --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/STObjectField.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class STObjectField implements HasField{} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Type.java b/ripple-core/src/main/java/com/ripple/core/fields/Type.java new file mode 100644 index 0000000000..2384861e88 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/Type.java @@ -0,0 +1,35 @@ +/* DO NOT EDIT, AUTO GENERATED */ +package com.ripple.core.fields; + +public enum Type { + Unknown(-2), + Done(-1), + NotPresent(0), + UInt16(1), + UInt32(2), + UInt64(3), + Hash128(4), + Hash256(5), + Amount(6), + Blob(7), + AccountID(8), + STObject(14), + STArray(15), + UInt8(16), + Hash160(17), + PathSet(18), + Vector256(19), + Transaction(10001), + LedgerEntry(10002), + Validation(10003); + + final int id; + + Type(int type) { + this.id = type; + } + + public int getId() { + return id; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/UInt16Field.java b/ripple-core/src/main/java/com/ripple/core/fields/UInt16Field.java new file mode 100644 index 0000000000..fa99de6499 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/UInt16Field.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class UInt16Field implements HasField {} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/UInt32Field.java b/ripple-core/src/main/java/com/ripple/core/fields/UInt32Field.java new file mode 100644 index 0000000000..a2b58902c2 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/UInt32Field.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class UInt32Field implements HasField {} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/UInt64Field.java b/ripple-core/src/main/java/com/ripple/core/fields/UInt64Field.java new file mode 100644 index 0000000000..3c85f3b312 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/UInt64Field.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class UInt64Field implements HasField {} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/UInt8Field.java b/ripple-core/src/main/java/com/ripple/core/fields/UInt8Field.java new file mode 100644 index 0000000000..465a1ad6be --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/UInt8Field.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class UInt8Field implements HasField {} diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Vector256Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Vector256Field.java new file mode 100644 index 0000000000..7cadc88ea8 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/fields/Vector256Field.java @@ -0,0 +1,3 @@ +package com.ripple.core.fields; + +public abstract class Vector256Field implements HasField{} diff --git a/ripple-core/src/main/java/com/ripple/core/formats/Format.java b/ripple-core/src/main/java/com/ripple/core/formats/Format.java new file mode 100644 index 0000000000..0061213330 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/formats/Format.java @@ -0,0 +1,66 @@ +package com.ripple.core.formats; + +import com.ripple.core.fields.Field; + +import java.util.EnumMap; + +abstract public class Format { + protected Format() { + } + + protected void addCommonFields(){} + + EnumMap requirementEnumMap = new EnumMap<>(Field.class); + EnumMap common = new EnumMap<>(Field.class); + + public EnumMap requirements() { + return requirementEnumMap; + } + + abstract public String name (); + + public Format(Object[] args) { + if ((!(args.length % 2 == 0)) || args.length < 2) { + throw new IllegalArgumentException("Varargs length should be a minimum multiple of 2"); + } + for (int i = 0; i < args.length; i+= 2) { + Field f = (Field) args[i]; + Requirement r = (Requirement) args[i + 1]; + put(f, r); + } + } + + protected void put(Field f, Requirement r) { + requirementEnumMap.put(f, r); + } + + @SuppressWarnings("unchecked") + Subclass required(Field f) { + put(f, Requirement.REQUIRED); + return (Subclass) this; + } + + @SuppressWarnings("unchecked") + Subclass optional(Field f) { + put(f, Requirement.OPTIONAL); + return (Subclass) this; + } + + @SuppressWarnings("unchecked") + Subclass nonDefault(Field f) { + put(f, Requirement.DEFAULT); + return (Subclass) this; + } + + public boolean isCommon(Field field) { + return common.containsKey(field); + } + + public static enum Requirement { + INVALID(-1), + REQUIRED( 0), + OPTIONAL( 1), + DEFAULT( 2); + Requirement(int i) {} + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/formats/LEFormat.java b/ripple-core/src/main/java/com/ripple/core/formats/LEFormat.java new file mode 100644 index 0000000000..2dbd576593 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/formats/LEFormat.java @@ -0,0 +1,230 @@ +package com.ripple.core.formats; + +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.LedgerEntryType; + +import java.util.EnumMap; + +public class LEFormat extends Format { + static public EnumMap formats = new EnumMap(LedgerEntryType.class); + { + common.put(Field.LedgerIndex, Requirement.OPTIONAL); + common.put(Field.LedgerEntryType, Requirement.REQUIRED); + common.put(Field.Flags, Requirement.REQUIRED); + } + + static public LEFormat fromString(String name) { + return getLedgerFormat(LedgerEntryType.valueOf(name)); + } + + static public LEFormat fromNumber(Number ord) { + return getLedgerFormat(LedgerEntryType.fromNumber(ord)); + } + + static public LEFormat fromValue(Object o) { + if (o instanceof Number) { + return fromNumber(((Number) o).intValue()); + } else if (o instanceof String){ + return fromString((String) o); + } + else { + return null; + } + } + + public static LEFormat getLedgerFormat(LedgerEntryType key) { + if (key == null) return null; + return formats.get(key); + } + + public final LedgerEntryType ledgerEntryType; + + public LEFormat(LedgerEntryType type, Object... args) { + super(args); + ledgerEntryType = type; + addCommonFields(); + formats.put(type, this); + } + + public LEFormat(LedgerEntryType type) { + super(); + ledgerEntryType = type; + addCommonFields(); + formats.put(type, this); + } + + @Override + protected void addCommonFields() { + requirementEnumMap.putAll(common); + } + + @Override + public String name() { + return ledgerEntryType.toString(); + } + + public static LEFormat AccountRoot = new LEFormat( + LedgerEntryType.AccountRoot, + Field.Account, Requirement.REQUIRED, + Field.Sequence, Requirement.REQUIRED, + Field.Balance, Requirement.REQUIRED, + Field.OwnerCount, Requirement.REQUIRED, + Field.PreviousTxnID, Requirement.REQUIRED, + Field.PreviousTxnLgrSeq, Requirement.REQUIRED, + Field.AccountTxnID, Requirement.OPTIONAL, + Field.RegularKey, Requirement.OPTIONAL, + Field.EmailHash, Requirement.OPTIONAL, + Field.WalletLocator, Requirement.OPTIONAL, + Field.WalletSize, Requirement.OPTIONAL, + Field.MessageKey, Requirement.OPTIONAL, + Field.TransferRate, Requirement.OPTIONAL, + Field.Domain, Requirement.OPTIONAL, + Field.TickSize, Requirement.OPTIONAL + ); + + public static LEFormat DirectoryNode = new LEFormat( + LedgerEntryType.DirectoryNode, + Field.Owner, Requirement.OPTIONAL, // for owner directories + Field.TakerPaysCurrency, Requirement.OPTIONAL, // for order book directories + Field.TakerPaysIssuer, Requirement.OPTIONAL, // for order book directories + Field.TakerGetsCurrency, Requirement.OPTIONAL, // for order book directories + Field.TakerGetsIssuer, Requirement.OPTIONAL, // for order book directories + Field.ExchangeRate, Requirement.OPTIONAL, // for order book directories + Field.Indexes, Requirement.REQUIRED, + Field.RootIndex, Requirement.REQUIRED, + Field.IndexNext, Requirement.OPTIONAL, + Field.IndexPrevious, Requirement.OPTIONAL + ); + + + public static LEFormat Offer = new LEFormat( + LedgerEntryType.Offer, + Field.Account, Requirement.REQUIRED, + Field.Sequence, Requirement.REQUIRED, + Field.TakerPays, Requirement.REQUIRED, + Field.TakerGets, Requirement.REQUIRED, + Field.BookDirectory, Requirement.REQUIRED, + Field.BookNode, Requirement.REQUIRED, + Field.OwnerNode, Requirement.REQUIRED, + Field.PreviousTxnID, Requirement.REQUIRED, + Field.PreviousTxnLgrSeq, Requirement.REQUIRED, + Field.Expiration, Requirement.OPTIONAL + ); + + public static LEFormat Ticket = new LEFormat( + LedgerEntryType.Ticket, + Field.Account, Requirement.REQUIRED, + Field.Sequence, Requirement.REQUIRED, + Field.OwnerNode, Requirement.REQUIRED, + Field.Target, Requirement.OPTIONAL, + Field.Expiration, Requirement.OPTIONAL + ); + + public static LEFormat RippleState = new LEFormat( + LedgerEntryType.RippleState, + Field.Balance, Requirement.REQUIRED, + Field.LowLimit, Requirement.REQUIRED, + Field.HighLimit, Requirement.REQUIRED, + Field.PreviousTxnID, Requirement.REQUIRED, + Field.PreviousTxnLgrSeq, Requirement.REQUIRED, + Field.LowNode, Requirement.OPTIONAL, + Field.LowQualityIn, Requirement.OPTIONAL, + Field.LowQualityOut, Requirement.OPTIONAL, + Field.HighNode, Requirement.OPTIONAL, + Field.HighQualityIn, Requirement.OPTIONAL, + Field.HighQualityOut, Requirement.OPTIONAL + ); + + public static LEFormat Escrow = new LEFormat( + LedgerEntryType.Escrow, + Field.Account, Requirement.REQUIRED, + Field.Destination, Requirement.REQUIRED, + Field.Amount, Requirement.REQUIRED, + + Field.PreviousTxnID, Requirement.REQUIRED, + Field.PreviousTxnLgrSeq, Requirement.REQUIRED, + Field.OwnerNode, Requirement.REQUIRED, + + Field.Condition, Requirement.OPTIONAL, + Field.CancelAfter, Requirement.OPTIONAL, + Field.FinishAfter, Requirement.OPTIONAL, + Field.SourceTag, Requirement.OPTIONAL, + Field.DestinationTag, Requirement.OPTIONAL, + Field.DestinationNode, Requirement.OPTIONAL + ); + + public static LEFormat LedgerHashes = new LEFormat( + LedgerEntryType.LedgerHashes, + Field.FirstLedgerSequence, Requirement.OPTIONAL, // Remove if we do a ledger restart + Field.LastLedgerSequence, Requirement.OPTIONAL, + Field.Hashes, Requirement.REQUIRED + ); + + public static LEFormat Amendments = new LEFormat( + LedgerEntryType.Amendments, + Field.Amendments, Requirement.OPTIONAL, + Field.Majorities, Requirement.OPTIONAL + ); + + public static LEFormat SignerList = new LEFormat( + LedgerEntryType.SignerList, + + Field.PreviousTxnID, Requirement.REQUIRED, + Field.PreviousTxnLgrSeq, Requirement.REQUIRED, + Field.OwnerNode, Requirement.REQUIRED, + + Field.SignerQuorum, Requirement.REQUIRED, + Field.SignerEntries, Requirement.REQUIRED, + Field.SignerListID, Requirement.REQUIRED + ); + + public static LEFormat FeeSettings = new LEFormat( + LedgerEntryType.FeeSettings, + Field.BaseFee, Requirement.REQUIRED, + Field.ReferenceFeeUnits, Requirement.REQUIRED, + Field.ReserveBase, Requirement.REQUIRED, + Field.ReserveIncrement, Requirement.REQUIRED + ); + + public static LEFormat PayChannel = new LEFormat( + LedgerEntryType.PayChannel, + Field.Account, Requirement.REQUIRED, + Field.Destination, Requirement.REQUIRED, + Field.Amount, Requirement.REQUIRED, + Field.Balance, Requirement.REQUIRED, + Field.PublicKey, Requirement.REQUIRED, + Field.SettleDelay, Requirement.REQUIRED, + Field.Expiration, Requirement.OPTIONAL, + Field.CancelAfter, Requirement.OPTIONAL, + Field.SourceTag, Requirement.OPTIONAL, + Field.DestinationTag, Requirement.OPTIONAL, + Field.OwnerNode, Requirement.REQUIRED, + Field.PreviousTxnID, Requirement.REQUIRED, + Field.PreviousTxnLgrSeq, Requirement.REQUIRED + ); + + public static LEFormat Check = new LEFormat(LedgerEntryType.Check) + .required(Field.PreviousTxnID) + .required(Field.PreviousTxnLgrSeq) + .required(Field.Account) + .required(Field.Destination) + .required(Field.SendMax) + .required(Field.Sequence) + .required(Field.OwnerNode) + .required(Field.DestinationNode) + .optional(Field.Expiration) + .optional(Field.InvoiceID) + .optional(Field.SourceTag) + .optional(Field.DestinationTag) + + ; + + public static LEFormat DepositPreauth = new LEFormat(LedgerEntryType.DepositPreauth) + .required(Field.Account) + .required(Field.Authorize) + .required(Field.OwnerNode) + .required(Field.PreviousTxnID) + .required(Field.PreviousTxnLgrSeq) + ; + +} diff --git a/ripple-core/src/main/java/com/ripple/core/formats/TxFormat.java b/ripple-core/src/main/java/com/ripple/core/formats/TxFormat.java new file mode 100644 index 0000000000..fa66d27533 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/formats/TxFormat.java @@ -0,0 +1,212 @@ +package com.ripple.core.formats; + +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.TransactionType; + +import java.util.EnumMap; + +public class TxFormat extends Format { + static public EnumMap formats = new EnumMap<>(TransactionType.class); + { + common.put(Field.TransactionType, Requirement.REQUIRED); + common.put(Field.Account, Requirement.REQUIRED); + common.put(Field.Sequence, Requirement.REQUIRED); + common.put(Field.Fee, Requirement.REQUIRED); + common.put(Field.SigningPubKey, Requirement.REQUIRED); + + common.put(Field.Flags, Requirement.OPTIONAL); + common.put(Field.SourceTag, Requirement.OPTIONAL); + common.put(Field.PreviousTxnID, Requirement.OPTIONAL); + common.put(Field.OperationLimit, Requirement.OPTIONAL); + common.put(Field.TxnSignature, Requirement.OPTIONAL); + common.put(Field.AccountTxnID, Requirement.OPTIONAL); + common.put(Field.LastLedgerSequence, Requirement.OPTIONAL); + common.put(Field.Memos, Requirement.OPTIONAL); + common.put(Field.Signers, Requirement.OPTIONAL); + + } + public final TransactionType transactionType; + + static public TxFormat fromString(String name) { + return getTxFormat(TransactionType.valueOf(name)); + } + + static public TxFormat fromNumber(Number ord) { + return getTxFormat(TransactionType.fromNumber(ord)); + } + + private static TxFormat getTxFormat(TransactionType key) { + if (key == null) return null; + return formats.get(key); + } + + public TxFormat(TransactionType type, Object... args) { + super(args); + transactionType = type; + addCommonFields(); + formats.put(transactionType, this); + } + + public TxFormat(TransactionType type) { + super(); + transactionType = type; + addCommonFields(); + formats.put(transactionType, this); + } + + @Override + protected void addCommonFields() { + requirementEnumMap.putAll(common); + } + + @Override + public String name() { + return transactionType.toString(); + } + + private static TxFormat makeFormat(TransactionType tt) { + return new TxFormat(tt); + } + + static public TxFormat AccountSet = makeFormat( + TransactionType.AccountSet) + .optional(Field.EmailHash) + .optional(Field.EmailHash) + .optional(Field.WalletLocator) + .optional(Field.WalletSize) + .optional(Field.MessageKey) + .optional(Field.Domain) + .optional(Field.TransferRate) + .optional(Field.SetFlag) + .optional(Field.TickSize) + .optional(Field.ClearFlag); + + static public TxFormat TrustSet = new TxFormat( + TransactionType.TrustSet, + Field.LimitAmount, Requirement.OPTIONAL, + Field.QualityIn, Requirement.OPTIONAL, + Field.QualityOut, Requirement.OPTIONAL); + + static public TxFormat OfferCreate = new TxFormat( + TransactionType.OfferCreate, + Field.TakerPays, Requirement.REQUIRED, + Field.TakerGets, Requirement.REQUIRED, + Field.Expiration, Requirement.OPTIONAL, + Field.OfferSequence, Requirement.OPTIONAL); + + static public TxFormat OfferCancel = new TxFormat( + TransactionType.OfferCancel, + Field.OfferSequence, Requirement.REQUIRED); + + static public TxFormat TicketCreate = new TxFormat( + TransactionType.TicketCreate, + Field.Target, Requirement.OPTIONAL, + Field.Expiration, Requirement.OPTIONAL); + + static public TxFormat TicketCancel = new TxFormat( + TransactionType.TicketCancel, + Field.TicketID, Requirement.REQUIRED); + + static public TxFormat SetRegularKey = new TxFormat( + TransactionType.SetRegularKey, + Field.RegularKey, Requirement.OPTIONAL); + + static public TxFormat Payment = new TxFormat( + TransactionType.Payment, + Field.Destination, Requirement.REQUIRED, + Field.Amount, Requirement.REQUIRED, + Field.SendMax, Requirement.OPTIONAL, + Field.Paths, Requirement.DEFAULT, + Field.InvoiceID, Requirement.OPTIONAL, + Field.DestinationTag, Requirement.OPTIONAL, + Field.DeliverMin, Requirement.OPTIONAL + ); + + + static public TxFormat EscrowCreate = new TxFormat( + TransactionType.EscrowCreate, + Field.Destination, Requirement.REQUIRED, + Field.Amount, Requirement.REQUIRED, + Field.Condition, Requirement.OPTIONAL, + Field.CancelAfter, Requirement.OPTIONAL, + Field.FinishAfter, Requirement.OPTIONAL, + Field.DestinationTag, Requirement.OPTIONAL); + + static public TxFormat EscrowFinish = new TxFormat( + TransactionType.EscrowFinish, + Field.Owner, Requirement.REQUIRED, + Field.OfferSequence, Requirement.REQUIRED, + Field.Fulfillment, Requirement.OPTIONAL, + Field.Condition, Requirement.OPTIONAL); + + static public TxFormat EscrowCancel = new TxFormat( + TransactionType.EscrowCancel, + Field.Owner, Requirement.REQUIRED, + Field.OfferSequence, Requirement.REQUIRED); + + static public TxFormat EnableAmendment = new TxFormat( + TransactionType.EnableAmendment, + Field.LedgerSequence, Requirement.REQUIRED, + Field.Amendment, Requirement.REQUIRED); + + static public TxFormat SetFee = new TxFormat( + TransactionType.SetFee, + Field.BaseFee, Requirement.REQUIRED, + Field.ReferenceFeeUnits, Requirement.REQUIRED, + Field.ReserveBase, Requirement.REQUIRED, + Field.LedgerSequence, Requirement.OPTIONAL, + Field.ReserveIncrement, Requirement.REQUIRED + ); + + static public TxFormat SignerListSet = new TxFormat( + TransactionType.SignerListSet, + Field.SignerQuorum, Requirement.REQUIRED, + Field.SignerEntries, Requirement.OPTIONAL + ); + static public TxFormat PaymentChannelCreate = new TxFormat( + TransactionType.PaymentChannelCreate, + Field.Destination, Requirement.REQUIRED, + Field.Amount, Requirement.REQUIRED, + Field.SettleDelay, Requirement.REQUIRED, + Field.PublicKey, Requirement.REQUIRED, + Field.CancelAfter, Requirement.OPTIONAL, + Field.DestinationTag, Requirement.OPTIONAL + ); + static public TxFormat PaymentChannelFund = new TxFormat( + TransactionType.PaymentChannelFund, + Field.Channel, Requirement.REQUIRED, + Field.Amount, Requirement.REQUIRED, + Field.Expiration, Requirement.OPTIONAL + ); + static public TxFormat PaymentChannelClaim = new TxFormat( + TransactionType.PaymentChannelClaim, + Field.Channel, Requirement.REQUIRED, + Field.Amount, Requirement.OPTIONAL, + Field.Balance, Requirement.OPTIONAL, + Field.Signature, Requirement.OPTIONAL, + Field.PublicKey, Requirement.OPTIONAL + ); + + static public TxFormat CheckCreate = new TxFormat(TransactionType.CheckCreate) + .required(Field.Destination) + .required(Field.SendMax) + .optional(Field.Expiration) + .optional(Field.DestinationTag) + .optional(Field.InvoiceID) + ; + + static public TxFormat CheckCash = new TxFormat(TransactionType.CheckCash) + .required(Field.CheckID) + .optional(Field.Amount) + .optional(Field.DeliverMin) + ; + + static public TxFormat CheckCancel = new TxFormat(TransactionType.CheckCancel) + .required(Field.CheckID) + ; + + static public TxFormat DepositPreauth = new TxFormat(TransactionType.DepositPreauth) + .optional(Field.Authorize) + .optional(Field.Unauthorize) + ; +} diff --git a/ripple-core/src/main/java/com/ripple/core/runtime/Value.java b/ripple-core/src/main/java/com/ripple/core/runtime/Value.java new file mode 100644 index 0000000000..1f69e88c55 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/runtime/Value.java @@ -0,0 +1,111 @@ +package com.ripple.core.runtime; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; +import java.util.Map; + +public enum Value { + UNKNOWN, + STRING, + JSON_OBJECT, + JSON_ARRAY, + + JACKSON_ARRAY, + JACKSON_BINARY, + JACKSON_BOOLEAN, + JACKSON_MISSING, + JACKSON_NULL, + JACKSON_NUMBER, + JACKSON_OBJECT, + JACKSON_POJO, + JACKSON_STRING, + + LIST, + MAP, + NUMBER, + BYTE, + DOUBLE, + FLOAT, + INTEGER, + LONG, + BYTE_ARRAY, + SHORT, + BOOLEAN; + + static public Value typeOf (Object object) { + if (object instanceof String) { + return STRING; + } + else if (object instanceof Number) { + if (object instanceof Byte) { + return BYTE; + } + else if (object instanceof Double) { + return DOUBLE; + } + else if (object instanceof Float) { + return FLOAT; + } + else if (object instanceof Integer) { + return INTEGER; + } + else if (object instanceof Long) { + return LONG; + } + else if (object instanceof Short) { + return SHORT; + } + return NUMBER; + } + else if (object instanceof JSONObject) { + return JSON_OBJECT; + } + else if (object instanceof JSONArray) { + return JSON_ARRAY; + } + else if (object instanceof JsonNode) { + JsonNode node = (JsonNode) object; + JsonNodeType nodeType = node.getNodeType(); + switch (nodeType) { + case ARRAY: + return JACKSON_ARRAY; + case BINARY: + return JACKSON_BINARY; + case BOOLEAN: + return JACKSON_BOOLEAN; + case MISSING: + return JACKSON_MISSING; + case NULL: + return JACKSON_NULL; + case NUMBER: + return JACKSON_NUMBER; + case OBJECT: + return JACKSON_OBJECT; + case POJO: + return JACKSON_POJO; + case STRING: + return JACKSON_STRING; + } + throw new IllegalStateException("unknown node type"); + } + else if (object instanceof Map) { + return MAP; + } + else if (object instanceof Boolean) { + return BOOLEAN; + } + else if (object instanceof List) { + return LIST; + } + else if (object instanceof byte[]) { + return BYTE_ARRAY; + } + else { + return UNKNOWN; + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/BinaryParser.java b/ripple-core/src/main/java/com/ripple/core/serialized/BinaryParser.java new file mode 100644 index 0000000000..86fa08a7d9 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/BinaryParser.java @@ -0,0 +1,99 @@ +package com.ripple.core.serialized; + +import com.ripple.core.fields.Field; +import com.ripple.encodings.common.B16; + +public class BinaryParser { + protected final int size; + protected byte[] bytes; + protected int cursor = 0; + + public BinaryParser(byte[] bytes) { + this.size = bytes.length; + this.bytes = bytes; + } + + public BinaryParser(int size) { + this.size = size; + } + + public BinaryParser(String hex) { + this(B16.decode(hex)); + } + + public void skip(int n) { + cursor += n; + } + public byte readOne() { + return bytes[cursor++]; + } + public byte[] read(int n) { + byte[] ret = new byte[n]; + System.arraycopy(bytes, cursor, ret, 0, n); + cursor += n; +// } + return ret; + } + + public Field readField() { + int fieldCode = readFieldCode(); + Field field = Field.fromCode(fieldCode); + if (field == null) { + throw new IllegalStateException("Couldn't parse field from " + + Integer.toHexString(fieldCode)); + } + return field; + } + + public int readFieldCode() { + byte tagByte = readOne(); + + int typeBits = (tagByte & 0xFF) >>> 4; + if (typeBits == 0) typeBits = readOne(); + + int fieldBits = tagByte & 0x0F; + if (fieldBits == 0) fieldBits = readOne(); + + return (typeBits << 16 | fieldBits); + } + + public boolean end() { + return cursor >= size; // greater guard against infinite loops + } + + public int pos() { + return cursor; + } + + public int readOneInt() { + return readOne() & 0xFF; + } + + public int readVLLength() { + int b1 = readOneInt(); + int result; + + if (b1 <= 192) { + result = b1; + } else if (b1 <= 240) { + int b2 = readOneInt(); + result = 193 + (b1 - 193) * 256 + b2; + } else if (b1 <= 254) { + int b2 = readOneInt(); + int b3 = readOneInt(); + result = 12481 + (b1 - 241) * 65536 + b2 * 256 + b3; + } else { + throw new RuntimeException("Invalid varint length indicator"); + } + + return result; + } + + public int size() { + return size; + } + + public boolean end(Integer customEnd) { + return cursor >= size || customEnd != null && cursor >= customEnd; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/BinarySerializer.java b/ripple-core/src/main/java/com/ripple/core/serialized/BinarySerializer.java new file mode 100644 index 0000000000..1b39a2ac10 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/BinarySerializer.java @@ -0,0 +1,94 @@ +package com.ripple.core.serialized; + +import com.ripple.core.fields.Field; +import com.ripple.core.fields.Type; + +import java.util.Arrays; + +public class BinarySerializer { + private final BytesSink sink; + + public BinarySerializer(BytesSink sink) { + this.sink = sink; + } + + public void add(byte[] n) { + sink.add(n); + } + + public void addLengthEncoded(byte[] n) { + add(encodeVL(n.length)); + add(n); + } + + public static byte[] encodeVL(int length) { + // TODO: bytes + byte[] lenBytes = new byte[4]; + + if (length <= 192) + { + lenBytes[0] = (byte) (length); + return Arrays.copyOf(lenBytes, 1); + } + else if (length <= 12480) + { + length -= 193; + lenBytes[0] = (byte) (193 + (length >>> 8)); + lenBytes[1] = (byte) (length & 0xff); + return Arrays.copyOf(lenBytes, 2); + } + else if (length <= 918744) { + length -= 12481; + lenBytes[0] = (byte) (241 + (length >>> 16)); + lenBytes[1] = (byte) ((length >> 8) & 0xff); + lenBytes[2] = (byte) (length & 0xff); + return Arrays.copyOf(lenBytes, 3); + } else { + throw new RuntimeException("Overflow error"); + } + } + + public void add(BytesList bl) { + for (byte[] bytes : bl.rawList()) { + sink.add(bytes); + } + } + + public int addFieldHeader(Field f) { + if (!f.isSerialized()) { + throw new IllegalStateException(String.format("Field %s is a discardable field", f)); + } + byte[] n = f.getBytes(); + add(n); + return n.length; + } + + public void add(byte type) { + sink.add(type); + } + + public void addLengthEncoded(BytesList bytes) { + add(encodeVL(bytes.bytesLength())); + add(bytes); + } + + public void add(Field field, SerializedType value) { + addFieldHeader(field); + if (field.isVLEncoded()) { + addLengthEncoded(value); + } else { + value.toBytesSink(sink); + if (field.getType() == Type.STObject) { + addFieldHeader(Field.ObjectEndMarker); + } else if (field.getType() == Type.STArray) { + addFieldHeader(Field.ArrayEndMarker); + } + } + } + + public void addLengthEncoded(SerializedType value) { + BytesList bytes = new BytesList(); + value.toBytesSink(bytes); + addLengthEncoded(bytes); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/BytesList.java b/ripple-core/src/main/java/com/ripple/core/serialized/BytesList.java new file mode 100644 index 0000000000..69d150aa16 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/BytesList.java @@ -0,0 +1,77 @@ +package com.ripple.core.serialized; + +import java.security.MessageDigest; +import java.util.ArrayList; + +public class BytesList implements BytesSink { + private ArrayList buffer = new ArrayList(); + + private int len = 0; + + public void add(BytesList bl) { + for (byte[] bytes : bl.rawList()) { + add(bytes); + } + } + + @Override + public void add(byte aByte) { + add(new byte[]{aByte}); + } + + @Override + public void add(byte[] bytes) { + len += bytes.length; + buffer.add(bytes); + } + + public byte[] bytes() { + int n = bytesLength(); + byte[] bytes = new byte[n]; + addBytes(bytes, 0); + return bytes; + } + + static public String[] hexLookup = new String[256]; + static { + for (int i = 0; i < 256; i++) { + String s = Integer.toHexString(i).toUpperCase(); + if (s.length() == 1) { + s = "0" + s; + } + hexLookup[i] = s; + } + } + + public String bytesHex() { + StringBuilder builder = new StringBuilder(len * 2); + for (byte[] buf : buffer) { + for (byte aBytes : buf) { + builder.append(hexLookup[aBytes & 0xFF]); + } + } + return builder.toString(); + } + + public int bytesLength() { + return len; + } + + private int addBytes(byte[] bytes, int destPos) { + for (byte[] buf : buffer) { + System.arraycopy(buf, 0, bytes, destPos, buf.length); + destPos += buf.length; + } + return destPos; + } + + public void updateDigest(MessageDigest digest) { + for (byte[] buf : buffer) { + digest.update(buf); + } + } + + public ArrayList rawList() { + return buffer; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/BytesSink.java b/ripple-core/src/main/java/com/ripple/core/serialized/BytesSink.java new file mode 100644 index 0000000000..7ec5c6cc5a --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/BytesSink.java @@ -0,0 +1,8 @@ +package com.ripple.core.serialized; + +public interface BytesSink { + default void add(byte aByte) { + add(new byte[] {aByte}); + } + void add(byte[] bytes); +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/MultiSink.java b/ripple-core/src/main/java/com/ripple/core/serialized/MultiSink.java new file mode 100644 index 0000000000..02de05e311 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/MultiSink.java @@ -0,0 +1,16 @@ +package com.ripple.core.serialized; + +public class MultiSink implements BytesSink { + final private BytesSink[] sinks; + public MultiSink(BytesSink... sinks) { + this.sinks = sinks; + } + @Override + public void add(byte b) { + for (BytesSink sink : sinks) sink.add(b); + } + @Override + public void add(byte[] b) { + for (BytesSink sink : sinks) sink.add(b); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/SerializedType.java b/ripple-core/src/main/java/com/ripple/core/serialized/SerializedType.java new file mode 100644 index 0000000000..1b5263e935 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/SerializedType.java @@ -0,0 +1,11 @@ +package com.ripple.core.serialized; + +import com.ripple.core.fields.Type; + +public interface SerializedType { + Object toJSON(); + byte[] toBytes(); + String toHex(); + void toBytesSink(BytesSink to); + Type type(); +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/StreamBinaryParser.java b/ripple-core/src/main/java/com/ripple/core/serialized/StreamBinaryParser.java new file mode 100644 index 0000000000..b3f964c31d --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/StreamBinaryParser.java @@ -0,0 +1,81 @@ +package com.ripple.core.serialized; + +import java.io.*; +import java.util.zip.GZIPInputStream; + +public class StreamBinaryParser extends BinaryParser { + final BufferedInputStream stream; + + public StreamBinaryParser(InputStream stream, long size) { + super((int) size); + this.stream = new BufferedInputStream(stream); + } + + private static boolean isGZip(File fio) { + return fio.getName().endsWith("gz"); + } + private static int getUncompressedSize(File fio) { + if (isGZip(fio)) { + int val; + try { + RandomAccessFile raf = new RandomAccessFile(fio, "r"); + raf.seek(raf.length() - 4); + int b4 = raf.read(); + int b3 = raf.read(); + int b2 = raf.read(); + int b1 = raf.read(); + val = (b1 << 24) | (b2 << 16) + (b3 << 8) + b4; + raf.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return val; + } else { + return (int) fio.length(); + } + } + + public void skip(int n) { + try { + long skipped = stream.skip(n); + if (skipped != n) { + throw new RuntimeException("Expected to skip more bytes"); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + public byte readOne() { + return read(1)[0]; + } + public byte[] read(int n) { + byte[] ret = new byte[n]; + try { + int read = stream.read(ret); + if (read != n) { + throw new RuntimeException("Expected to read more bytes"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + cursor += n; + return ret; + } + public static StreamBinaryParser fromFile(String path) { + try { + File f = new File(path); + FileInputStream fstream = new FileInputStream(path); + InputStream stream = fstream; + long s = fstream.getChannel().size(); + + if (isGZip(f)) { + s = getUncompressedSize(f); + stream = new GZIPInputStream(fstream); + } + return new StreamBinaryParser(stream, s); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/StreamSink.java b/ripple-core/src/main/java/com/ripple/core/serialized/StreamSink.java new file mode 100644 index 0000000000..34fe16d377 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/StreamSink.java @@ -0,0 +1,31 @@ +package com.ripple.core.serialized; + +import java.io.IOException; +import java.io.OutputStream; + +public class StreamSink implements BytesSink { + OutputStream out; + + public StreamSink(OutputStream out) { + this.out = out; + } + + @Override + public void add(byte aByte) { + try { + out.write(aByte); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void add(byte[] bytes) { + try { + out.write(bytes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} + diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/TypeTranslator.java b/ripple-core/src/main/java/com/ripple/core/serialized/TypeTranslator.java new file mode 100644 index 0000000000..4bd988338b --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/TypeTranslator.java @@ -0,0 +1,151 @@ + +package com.ripple.core.serialized; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.ripple.core.runtime.Value; +import com.ripple.encodings.common.B16; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * @param The SerializedType class + * TODO, this should only really have methods that each class over-rides + * it's currently pretty NASTY + */ +public abstract class TypeTranslator { + + @SuppressWarnings("unchecked") + public T fromValue(Object object) { + switch (Value.typeOf(object)) { + case STRING: + return fromString((String) object); + case JACKSON_ARRAY: + return fromJacksonArray((ArrayNode) object); + case JACKSON_BINARY: + throw new IllegalStateException("cant handle jackson binary"); + case JACKSON_BOOLEAN: + return fromBoolean(((JsonNode) object).asBoolean()); + case JACKSON_MISSING: + throw new IllegalStateException("cant create from missing"); + case JACKSON_NULL: + throw new IllegalStateException("cant create from null"); + case JACKSON_NUMBER: + JsonNode node = (JsonNode) object; + if (node.isLong()) { + return fromLong(node.asLong()); + } else if (node.isInt()) { + return fromInteger(node.asInt()); + } else if (node.isDouble()) { + return fromDouble(node.asDouble()); + } else if (node.isFloat()) { + return fromDouble(node.floatValue()); + } + throw new IllegalStateException("cant create from null"); + case JACKSON_OBJECT: + return fromJacksonObject((ObjectNode) object); + case JACKSON_POJO: + throw new IllegalStateException("cant handle POJO"); + case JACKSON_STRING: + return fromString(((JsonNode) object).asText()); + case DOUBLE: + return fromDouble((Double) object); + case INTEGER: + return fromInteger((Integer) object); + case LONG: + return fromLong((Long) object); + case BOOLEAN: + return fromBoolean((Boolean) object); + case JSON_ARRAY: + return fromJSONArray((JSONArray) object); + case JSON_OBJECT: + return fromJSONObject((JSONObject) object); + case BYTE_ARRAY: + return fromBytes((byte[]) object); + case UNKNOWN: + default: + return (T) object; + } + + } + + protected T fromJacksonObject(ObjectNode object) { + throw new UnsupportedOperationException(); + } + + protected T fromJacksonArray(ArrayNode node) { + throw new UnsupportedOperationException(); + } + + public String toString(T obj) { + return obj.toString(); + } + + protected T fromJSONObject(JSONObject jsonObject) { + throw new UnsupportedOperationException(); + } + + protected T fromJSONArray(JSONArray jsonArray) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("WeakerAccess") + protected T fromBoolean(boolean aBoolean) { + throw new UnsupportedOperationException(); + } + + protected T fromLong(long aLong) { + throw new UnsupportedOperationException(); + } + + protected T fromInteger(int integer) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("WeakerAccess") + protected T fromDouble(double aDouble) { + throw new UnsupportedOperationException(); + } + + protected T fromString(String value) { + throw new UnsupportedOperationException(); + } + + /** + * @param hint Using a boxed integer, allowing null for no hint + */ + public abstract T fromParser(BinaryParser parser, Integer hint); + + public T fromParser(BinaryParser parser) { + return fromParser(parser, null); + } + + public T fromBytes(byte[] b) { + return fromParser(new BinaryParser(b)); + } + + public T fromHex(String hex) { + return fromBytes(B16.decode(hex)); + } + + public Object toJSON(T obj) { + return obj.toJSON(); + } + + public void toBytesSink(T obj, BytesSink to) { + obj.toBytesSink(to); + } + + public byte[] toBytes(T obj) { + BytesList to = new BytesList(); + toBytesSink(obj, to); + return to.bytes(); + } + + public String toHex(T obj) { + BytesList to = new BytesList(); + toBytesSink(obj, to); + return to.bytesHex(); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/enums/EngineResult.java b/ripple-core/src/main/java/com/ripple/core/serialized/enums/EngineResult.java new file mode 100644 index 0000000000..dafd5d761a --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/enums/EngineResult.java @@ -0,0 +1,258 @@ +package com.ripple.core.serialized.enums; + +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import com.ripple.encodings.common.B16; + +import java.util.TreeMap; + +class Holder { + public static int ords = 0; +} + +public enum EngineResult implements SerializedType { + telLOCAL_ERROR(-399, "Local failure."), + telBAD_DOMAIN("Domain too long."), + telBAD_PATH_COUNT("Malformed: Too many paths."), + telBAD_PUBLIC_KEY("Public key too long."), + telFAILED_PROCESSING("Failed to correctly process transaction."), + telINSUF_FEE_P("Fee insufficient."), + telNO_DST_PARTIAL("Partial payment to create account not allowed."), + + telCAN_NOT_QUEUE("Can not queue at this time."), + telCAN_NOT_QUEUE_BALANCE("Can not queue at this time: insufficient balance to pay all queued fees."), + telCAN_NOT_QUEUE_BLOCKS("Can not queue at this time: would block later queued transaction(s)."), + + telCAN_NOT_QUEUE_BLOCKED("Can not queue at this time: blocking transaction in queue."), + telCAN_NOT_QUEUE_FEE("Can not queue at this time: fee insufficient to replace queued transaction."), + telCAN_NOT_QUEUE_FULL("Can not queue at this time: queue is full."), + + temMALFORMED(-299, "Malformed transaction."), + temBAD_AMOUNT("Can only send positive amounts."), + temBAD_CURRENCY("Malformed: Bad currency."), + temBAD_EXPIRATION("Malformed: Bad expiration."), + temBAD_FEE("Invalid fee, negative or not XRP."), + temBAD_ISSUER("Malformed: Bad issuer."), + temBAD_LIMIT("Limits must be non-negative."), + temBAD_OFFER("Malformed: Bad offer."), + temBAD_PATH("Malformed: Bad path."), + temBAD_PATH_LOOP("Malformed: Loop in path."), + temBAD_SEND_XRP_LIMIT("Malformed: Limit quality is not allowed for XRP to XRP."), + temBAD_SEND_XRP_MAX("Malformed: Send max is not allowed for XRP to XRP."), + temBAD_SEND_XRP_NO_DIRECT("Malformed: No Ripple direct is not allowed for XRP to XRP."), + temBAD_SEND_XRP_PARTIAL("Malformed: Partial payment is not allowed for XRP to XRP."), + temBAD_SEND_XRP_PATHS("Malformed: Paths are not allowed for XRP to XRP."), + temBAD_SEQUENCE("Malformed: Sequence is not in the past."), + temBAD_SIGNATURE("Malformed: Bad signature."), + temBAD_SRC_ACCOUNT("Malformed: Bad source account."), + temBAD_TRANSFER_RATE("Malformed: Transfer rate must be >= 1.0 and <= 2.0"), + temDST_IS_SRC("Destination may not be source."), + temDST_NEEDED("Destination not specified."), + temINVALID("The transaction is ill-formed."), + temINVALID_FLAG("The transaction has an invalid flag."), + temREDUNDANT("Sends same currency to self."), + temRIPPLE_EMPTY("PathSet with no paths."), + temDISABLED("The transaction requires logic that is currently disabled."), + temBAD_SIGNER("Malformed: No signer may duplicate account or other signers."), + temBAD_QUORUM("Malformed: Quorum is unreachable."), + temBAD_WEIGHT("Malformed: Weight must be a positive value."), + temBAD_TICK_SIZE("Malformed: Tick size out of range."), + temINVALID_ACCOUNT_ID("Malformed: A field contains an invalid account ID."), + temCANNOT_PREAUTH_SELF("Malformed: An account may not preauthorize itself."), + temUNCERTAIN("In process of determining result. Never returned."), + temUNKNOWN(-266, "The transaction requires logic that is not implemented yet."), + + tefFAILURE(-199, "Failed to apply."), + tefALREADY("The exact transaction was already in this ledger."), + tefBAD_ADD_AUTH("Not authorized to add account."), + tefBAD_AUTH("Transaction's public key is not authorized."), + tefBAD_LEDGER("Ledger in unexpected state."), + tefCREATED("Can't add an already created account."), + tefEXCEPTION("Unexpected program state."), + tefINTERNAL("Internal error."), + tefNO_AUTH_REQUIRED("Auth is not required."), + tefPAST_SEQ("This sequence number has already passed."), + tefWRONG_PRIOR("This previous transaction does not match."), + tefMASTER_DISABLED("Master key is disabled."), + tefMAX_LEDGER("Ledger sequence too high."), + tefBAD_SIGNATURE("A signature is provided for a non-signer."), + tefBAD_QUORUM("Signatures provided do not meet the quorum."), + tefNOT_MULTI_SIGNING("Account has no appropriate list of multi-signers."), + tefBAD_AUTH_MASTER("Auth for unclaimed account needs correct master key."), + tefINVARIANT_FAILED("Fee claim violated invariants for the transaction."), + + + + + terRETRY(-99, "Retry transaction."), + terFUNDS_SPENT("Can't set password, password set funds already spent."), + terINSUF_FEE_B("Account balance can't pay fee."), + terNO_ACCOUNT("The source account does not exist."), + terNO_AUTH("Not authorized to hold IOUs."), + terNO_LINE("No such line."), + terOWNERS("Non-zero owner count."), + terPRE_SEQ("Missing/inapplicable prior transaction."), + terLAST("Process last."), + terNO_RIPPLE("Path does not permit rippling."), + terQUEUED("Held until escalated fee drops."), + + tesSUCCESS(0, "The transaction was applied. Only final in a validated ledger."), + + tecCLAIM(100, "Fee claimed. Sequence used. No action."), + tecPATH_PARTIAL(101, "Path could not send full amount."), + tecUNFUNDED_ADD(102, "Insufficient XRP balance for WalletAdd."), + tecUNFUNDED_OFFER(103, "Insufficient balance to fund created offer."), + tecUNFUNDED_PAYMENT(104, "Insufficient XRP balance to send."), + tecFAILED_PROCESSING(105, "Failed to correctly process transaction."), + tecDIR_FULL(121, "Can not add entry to full directory."), + tecINSUF_RESERVE_LINE(122, "Insufficient reserve to add trust line."), + tecINSUF_RESERVE_OFFER(123, "Insufficient reserve to create offer."), + tecNO_DST(124, "Destination does not exist. Send XRP to create it."), + tecNO_DST_INSUF_XRP(125, "Destination does not exist. Too little XRP sent to create it."), + tecNO_LINE_INSUF_RESERVE(126, "No such line. Too little reserve to create it."), + tecNO_LINE_REDUNDANT(127, "Can't set non-existent line to default."), + tecPATH_DRY(128, "Path could not send partial amount."), + tecUNFUNDED(129, "One of _ADD, _OFFER, or _SEND. Deprecated."), + tecNO_ALTERNATIVE_KEY(130, "The operation would remove the ability to sign transactions with the account."), + tecNO_REGULAR_KEY(131, "Regular key is not set."), + tecOWNERS(132, "Non-zero owner count."), + tecNO_ISSUER(133, "Issuer account does not exist."), + tecNO_AUTH(134, "Not authorized to hold asset."), + tecNO_LINE(135, "No such line."), + tecINSUFF_FEE(136, "Insufficient balance to pay fee."), + tecFROZEN(137, "Asset is frozen."), + tecNO_TARGET(138, "Target account does not exist."), + tecNO_PERMISSION(139, "No permission to perform requested operation."), + tecNO_ENTRY(140, "No matching entry found."), + tecINSUFFICIENT_RESERVE(141, "Insufficient reserve to complete requested operation."), + tecNEED_MASTER_KEY(142, "The operation requires the use of the Master Key."), + tecDST_TAG_NEEDED(143, "A destination tag is required."), + tecINTERNAL(144, "An internal error has occurred during processing."), + tecOVERSIZE(145, "Object exceeded serialization limits."), + tecCRYPTOCONDITION_ERROR(146, "Malformed, invalid, or mismatched conditional or fulfillment."), + tecINVARIANT_FAILED(147, "One or more invariants for the transaction were not satisfied."), + tecEXPIRED(148, "Expiration time is passed."), + tecDUPLICATE(149, "Ledger object already exists."), + tecKILLED(150, "FillOrKill offer killed."), + ; + + + public int asInteger() { + return ord; + } + + @Override + public Type type() { + return Type.UInt8; + } + + private static int ords = 0; + int ord; + public String human; + EngineResult class_ = null; + + EngineResult(Integer i, String s) { + human = s; + if (i == null) { + i = ++Holder.ords; + } else { + Holder.ords = i; + } + ord = i; + } + + EngineResult(String s) { + this(null, s); + } + + private static TreeMap byCode; + + static { + byCode = new TreeMap<>(); + for (EngineResult ter : EngineResult.values()) { + byCode.put(ter.ord, ter); + } + } + + public static EngineResult fromNumber(Number i) { + return byCode.get(i.intValue()); + } + + + /*Serialized Type implementation*/ + @Override + public byte[] toBytes() { + return new byte[]{(byte) ord}; + } + + @Override + public void toBytesSink(BytesSink to) { + to.add((byte) ord); + } + + @Override + public Object toJSON() { + return toString(); + } + + @Override + public String toHex() { + return B16.encode(toBytes()); + } + + public static class Translator extends TypeTranslator { + @Override + public EngineResult fromParser(BinaryParser parser, Integer hint) { + return fromInteger(parser.readOneInt()); + } + + @Override + public EngineResult fromString(String value) { + return EngineResult.valueOf(value); + } + + @Override + public EngineResult fromInteger(int integer) { + return fromNumber(integer); + } + } + + @SuppressWarnings("unused") + public static Translator translate = new Translator(); + + // Result Classes + public static EngineResult resultClass(EngineResult result) { + if (result.ord >= telLOCAL_ERROR.ord && result.ord < temMALFORMED.ord) { + return telLOCAL_ERROR; + } + if (result.ord >= temMALFORMED.ord && result.ord < tefFAILURE.ord) { + return temMALFORMED; + } + if (result.ord >= tefFAILURE.ord && result.ord < terRETRY.ord) { + return tefFAILURE; + } + if (result.ord >= terRETRY.ord && result.ord < tesSUCCESS.ord) { + return terRETRY; + } + if (result.ord >= tesSUCCESS.ord && result.ord < tecCLAIM.ord) { + return tesSUCCESS; + } + return tecCLAIM; + } + + public EngineResult resultClass() { + return class_; + } + + static { + for (EngineResult engineResult : EngineResult.values()) { + engineResult.class_ = resultClass(engineResult); + } + } + +} + + diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/enums/LedgerEntryType.java b/ripple-core/src/main/java/com/ripple/core/serialized/enums/LedgerEntryType.java new file mode 100644 index 0000000000..cba003d2d9 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/enums/LedgerEntryType.java @@ -0,0 +1,101 @@ +package com.ripple.core.serialized.enums; + +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import com.ripple.encodings.common.B16; + +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +public enum LedgerEntryType implements SerializedType { + // Invalid (-1), + AccountRoot('a'), + DirectoryNode('d'), + // GeneratorMap ('g'), + RippleState('r'), + Escrow('u'), + // Nickname ('n'), // deprecated + Offer('o'), + // Contract ('c'), + LedgerHashes('h'), + Amendments('f'), + FeeSettings('s'), + Ticket('T'), + SignerList('S'), + PayChannel('x'), + Check('C'), + DepositPreauth('p'); + + final int ord; + + LedgerEntryType(int i) { + ord = i; + } + + static private Map byCode = new TreeMap<>(); + + static { + for (Object a : LedgerEntryType.values()) { + LedgerEntryType f = (LedgerEntryType) a; + byCode.put(f.ord, f); + } + } + + @Override + public Type type() { + return Type.UInt16; + } + + public static LedgerEntryType fromNumber(Number i) { + return byCode.get(i.intValue()); + } + + public Integer asInteger() { + return ord; + } + + // SeralizedType interface + @Override + public byte[] toBytes() { + return new byte[]{(byte) ((ord >>> 8) & 0xFF), (byte) (ord & 0xFF)}; + } + + @Override + public Object toJSON() { + return toString(); + } + + @Override + public String toHex() { + return B16.encode(toBytes()); + } + + @Override + public void toBytesSink(BytesSink to) { + to.add(toBytes()); + } + + public static class Translator extends TypeTranslator { + @Override + public LedgerEntryType fromParser(BinaryParser parser, Integer hint) { + return fromNumber(parser.readOneInt() << 8 | parser.readOneInt()); + } + + @Override + public LedgerEntryType fromInteger(int integer) { + return fromNumber(integer); + } + + @Override + public LedgerEntryType fromString(String value) { + return LedgerEntryType.valueOf(value); + } + } + + @SuppressWarnings("unused") + public static Translator translate = new Translator(); +} diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/enums/TransactionType.java b/ripple-core/src/main/java/com/ripple/core/serialized/enums/TransactionType.java new file mode 100644 index 0000000000..7c98c3da07 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/serialized/enums/TransactionType.java @@ -0,0 +1,103 @@ +package com.ripple.core.serialized.enums; + +import com.ripple.core.fields.Type; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.TypeTranslator; +import com.ripple.encodings.common.B16; + +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +public enum TransactionType implements SerializedType { + // Invalid (-1), + Payment (0), + EscrowCreate (1), // open + EscrowFinish (2), + AccountSet (3), // open, + EscrowCancel (4), + SetRegularKey(5), + // unused1 (6), // open + OfferCreate (7), + OfferCancel (8), + // unused2(9), + TicketCreate(10), + TicketCancel(11), + SignerListSet(12), + PaymentChannelCreate(13), + PaymentChannelFund(14), + PaymentChannelClaim(15), + CheckCreate(16), + CheckCash(17), + CheckCancel(18), + DepositPreauth(19), + TrustSet (20), + EnableAmendment(100), + SetFee(101); + + public int asInteger() { + return ord; + } + + final int ord; + TransactionType(int i) { + ord = i; + } + + @Override + public Type type() { + return Type.UInt16; + } + + static private Map byCode = new TreeMap<>(); + static { + for (Object a : TransactionType.values()) { + TransactionType f = (TransactionType) a; + byCode.put(f.ord, f); + } + } + + static public TransactionType fromNumber(Number i) { + return byCode.get(i.intValue()); + } + + // SeralizedType interface + @Override + public byte[] toBytes() { + // TODO: bytes + return new byte[]{(byte) (ord >> 8), (byte) (ord & 0xFF)}; + } + @Override + public Object toJSON() { + return toString(); + } + @Override + public String toHex() { + return B16.encode(toBytes()); + } + @Override + public void toBytesSink(BytesSink to) { + to.add(toBytes()); + } + public static class Translator extends TypeTranslator { + @Override + public TransactionType fromParser(BinaryParser parser, Integer hint) { + return fromNumber(parser.readOneInt() << 8 | parser.readOneInt()); + } + + @Override + public TransactionType fromInteger(int integer) { + return fromNumber(integer); + } + + @Override + public TransactionType fromString(String value) { + return TransactionType.valueOf(value); + } + } + + @SuppressWarnings("unused") + public static Translator translate = new Translator(); +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/generic/Validation.java b/ripple-core/src/main/java/com/ripple/core/types/known/generic/Validation.java new file mode 100644 index 0000000000..f7efd08ed2 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/generic/Validation.java @@ -0,0 +1,15 @@ +package com.ripple.core.types.known.generic; + +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; + +public class Validation extends STObject { + public static boolean isValidation(STObject source) { + return source.has(UInt32.LedgerSequence) && + source.has(UInt32.SigningTime) && + source.has(Hash256.LedgerHash) && + source.has(Blob.Signature); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerEntry.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerEntry.java new file mode 100644 index 0000000000..c5dc3f7f3d --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerEntry.java @@ -0,0 +1,59 @@ +package com.ripple.core.types.known.sle; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.fields.Field; +import com.ripple.core.formats.LEFormat; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.entries.AccountRoot; +import com.ripple.core.types.known.sle.entries.DirectoryNode; +import com.ripple.core.types.known.sle.entries.Offer; +import com.ripple.core.types.known.sle.entries.RippleState; + +import java.util.TreeSet; + +public class LedgerEntry extends STObject { + public LedgerEntry(LedgerEntryType type) { + setFormat(LEFormat.formats.get(type)); + put(Field.LedgerEntryType, type); + } + + public LedgerEntryType ledgerEntryType() {return ledgerEntryType(this);} + public Hash256 index() { return get(Hash256.index); } + public UInt32 flags() {return get(UInt32.Flags);} + public Hash256 ledgerIndex() {return get(Hash256.LedgerIndex);} + + public void flags(UInt32 val) {put(Field.Flags, val);} + public void ledgerIndex(Hash256 val) {put(Field.LedgerIndex, val);} + + public boolean hasLedgerIndex() {return has(Hash256.LedgerIndex);} + + public TreeSet owners() { + TreeSet owners = new TreeSet<>(); + + if (has(Field.LowLimit)) { + owners.add(get(Amount.LowLimit).issuer()); + } + if (has(Field.HighLimit)) { + owners.add(get(Amount.HighLimit).issuer()); + } + if (has(Field.Account)) { + owners.add(get(AccountID.Account)); + } + + return owners; + } + + public void index(Hash256 index) { + put(Hash256.index, index); + } + + public void setDefaults() { + if (flags() == null) { + flags(UInt32.ZERO); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerHashes.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerHashes.java new file mode 100644 index 0000000000..9b9086cd72 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerHashes.java @@ -0,0 +1,32 @@ +package com.ripple.core.types.known.sle; + +import com.ripple.core.coretypes.Vector256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.LedgerEntryType; + +public class LedgerHashes extends LedgerEntry { + public LedgerHashes() { + super(LedgerEntryType.LedgerHashes); + } + + public Vector256 hashes() { + return get(Vector256.Hashes); + } + + public void hashes(Vector256 hashes) { + put(Vector256.Hashes, hashes); + } + + public UInt32 lastLedgerSequence() { + return get(UInt32.LastLedgerSequence); + } + + public UInt32 firstLedgerSequence() {return get(UInt32.FirstLedgerSequence);} + + public void firstLedgerSequence(UInt32 val) { put(UInt32.FirstLedgerSequence, val);} + + public void lastLedgerSequence(UInt32 val) { put(UInt32.LastLedgerSequence, val);} + + public boolean hasFirstLedgerSequence() {return has(UInt32.FirstLedgerSequence);} + public boolean hasLastLedgerSequence() {return has(UInt32.LastLedgerSequence);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/ThreadedLedgerEntry.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/ThreadedLedgerEntry.java new file mode 100644 index 0000000000..3739109d97 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/ThreadedLedgerEntry.java @@ -0,0 +1,18 @@ +package com.ripple.core.types.known.sle; + + +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.LedgerEntryType; + +// this class has a PreviousTxnID and PreviousTxnLgrSeq +abstract public class ThreadedLedgerEntry extends LedgerEntry { + public ThreadedLedgerEntry(LedgerEntryType type) { + super(type); + } + public UInt32 previousTxnLgrSeq() {return get(UInt32.PreviousTxnLgrSeq);} + public Hash256 previousTxnID() {return get(Hash256.PreviousTxnID);} + public void previousTxnLgrSeq(UInt32 val) {put(Field.PreviousTxnLgrSeq, val);} + public void previousTxnID(Hash256 val) {put(Field.PreviousTxnID, val);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/AccountRoot.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/AccountRoot.java new file mode 100644 index 0000000000..22f26def99 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/AccountRoot.java @@ -0,0 +1,71 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.hash.Hash128; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt8; +import com.ripple.core.enums.LedgerFlag; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.ThreadedLedgerEntry; + +public class AccountRoot extends ThreadedLedgerEntry { + public AccountRoot() { + super(LedgerEntryType.AccountRoot); + } + + public UInt32 sequence() {return get(UInt32.Sequence);} + public UInt32 transferRate() {return get(UInt32.TransferRate);} + public UInt32 walletSize() {return get(UInt32.WalletSize);} + public UInt32 ownerCount() {return get(UInt32.OwnerCount);} + public Hash128 emailHash() {return get(Hash128.EmailHash);} + public Hash256 walletLocator() {return get(Hash256.WalletLocator);} + public Amount balance() {return get(Amount.Balance);} + public Blob messageKey() {return get(Blob.MessageKey);} + public Blob domain() {return get(Blob.Domain);} + public AccountID account() {return get(AccountID.Account);} + public AccountID regularKey() {return get(AccountID.RegularKey);} + + public void sequence(UInt32 val) {put(Field.Sequence, val);} + public void transferRate(UInt32 val) {put(Field.TransferRate, val);} + public void walletSize(UInt32 val) {put(Field.WalletSize, val);} + public void ownerCount(UInt32 val) {put(Field.OwnerCount, val);} + public void emailHash(Hash128 val) {put(Field.EmailHash, val);} + public void walletLocator(Hash256 val) {put(Field.WalletLocator, val);} + public void balance(Amount val) {put(Field.Balance, val);} + public void messageKey(Blob val) {put(Field.MessageKey, val);} + public void domain(Blob val) {put(Field.Domain, val);} + public void account(AccountID val) {put(Field.Account, val);} + public void regularKey(AccountID val) {put(Field.RegularKey, val);} + + public boolean requiresAuth() { + return flags().testBit(LedgerFlag.RequireAuth); + } + + public boolean hasAccountTxnID() {return has(Hash256.AccountTxnID);} + public boolean hasDomain() {return has(Blob.Domain);} + public boolean hasEmailHash() {return has(Hash128.EmailHash);} + public boolean hasMessageKey() {return has(Blob.MessageKey);} + public boolean hasRegularKey() {return has(AccountID.RegularKey);} + public boolean hasTickSize() {return has(UInt8.TickSize);} + public boolean hasTransferRate() {return has(UInt32.TransferRate);} + public boolean hasWalletLocator() {return has(Hash256.WalletLocator);} + public boolean hasWalletSize() {return has(UInt32.WalletSize);} + + public Hash256 accountTxnID() {return get(Hash256.AccountTxnID);} + public UInt8 tickSize() {return get(UInt8.TickSize);} + + public void accountTxnID(Hash256 val) { put(Hash256.AccountTxnID, val);} + public void tickSize(UInt8 val) { put(UInt8.TickSize, val);} + + @Override + public void setDefaults() { + super.setDefaults(); + if (ownerCount() == null) { + ownerCount(new UInt32(0)); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Amendments.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Amendments.java new file mode 100644 index 0000000000..8b580af8ef --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Amendments.java @@ -0,0 +1,22 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.STArray; +import com.ripple.core.coretypes.Vector256; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.LedgerEntry; + +public class Amendments extends LedgerEntry { + public Amendments() { + super(LedgerEntryType.Amendments); + } + + public STArray majorities() {return get(STArray.Majorities);} + public Vector256 amendments() {return get(Vector256.Amendments);} + + public void amendments(Vector256 val) { put(Vector256.Amendments, val);} + public void majorities(STArray val) { put(STArray.Majorities, val);} + + public boolean hasAmendments() {return has(Vector256.Amendments);} + public boolean hasMajorities() {return has(STArray.Majorities);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Check.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Check.java new file mode 100644 index 0000000000..310232b1dd --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Check.java @@ -0,0 +1,40 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.serialized.enums.LedgerEntryType; + +public class Check extends IndexedLedgerEntry { + public Check() { + super(LedgerEntryType.Check); + } + + public boolean hasSourceTag() {return has(UInt32.SourceTag);} + public boolean hasExpiration() {return has(UInt32.Expiration);} + public boolean hasDestinationTag() {return has(UInt32.DestinationTag);} + public boolean hasInvoiceID() {return has(Hash256.InvoiceID);} + + public UInt32 sourceTag() {return get(UInt32.SourceTag);} + public UInt32 sequence() {return get(UInt32.Sequence);} + public UInt32 expiration() {return get(UInt32.Expiration);} + public UInt32 destinationTag() {return get(UInt32.DestinationTag);} + public UInt64 ownerNode() {return get(UInt64.OwnerNode);} + public UInt64 destinationNode() {return get(UInt64.DestinationNode);} + public Hash256 invoiceID() {return get(Hash256.InvoiceID);} + public Amount sendMax() {return get(Amount.SendMax);} + public AccountID account() {return get(AccountID.Account);} + public AccountID destination() {return get(AccountID.Destination);} + + public void sourceTag(UInt32 val) { put(UInt32.SourceTag, val);} + public void sequence(UInt32 val) { put(UInt32.Sequence, val);} + public void expiration(UInt32 val) { put(UInt32.Expiration, val);} + public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);} + public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);} + public void destinationNode(UInt64 val) { put(UInt64.DestinationNode, val);} + public void invoiceID(Hash256 val) { put(Hash256.InvoiceID, val);} + public void sendMax(Amount val) { put(Amount.SendMax, val);} + public void destination(AccountID val) { put(AccountID.Destination, val);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DepositPreauthLe.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DepositPreauthLe.java new file mode 100644 index 0000000000..7f995ecc1c --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DepositPreauthLe.java @@ -0,0 +1,18 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.serialized.enums.LedgerEntryType; + +public class DepositPreauthLe extends IndexedLedgerEntry { + public DepositPreauthLe() { + super(LedgerEntryType.DepositPreauth); + } + public UInt64 ownerNode() {return get(UInt64.OwnerNode);} + public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);} + + public AccountID account() {return get(AccountID.Account);} + + public AccountID authorize() {return get(AccountID.Authorize);} + public void authorize(AccountID val) { put(AccountID.Authorize, val);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DirectoryNode.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DirectoryNode.java new file mode 100644 index 0000000000..f46a7d7973 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DirectoryNode.java @@ -0,0 +1,88 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Currency; +import com.ripple.core.coretypes.Vector256; +import com.ripple.core.coretypes.hash.Hash160; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.Index; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.LedgerEntry; + +public class DirectoryNode extends LedgerEntry { + public DirectoryNode() { + super(LedgerEntryType.DirectoryNode); + } + + public UInt64 indexNext() {return get(UInt64.IndexNext);} + public UInt64 indexPrevious() {return get(UInt64.IndexPrevious);} + public UInt64 exchangeRate() {return get(UInt64.ExchangeRate);} + public Hash256 rootIndex() {return get(Hash256.RootIndex);} + public AccountID owner() {return get(AccountID.Owner);} + public Hash160 takerPaysCurrency() {return get(Hash160.TakerPaysCurrency);} + public Hash160 takerPaysIssuer() {return get(Hash160.TakerPaysIssuer);} + public Hash160 takerGetsCurrency() {return get(Hash160.TakerGetsCurrency);} + public Hash160 takerGetsIssuer() {return get(Hash160.TakerGetsIssuer);} + public Vector256 indexes() {return get(Vector256.Indexes);} + public void indexNext(UInt64 val) {put(Field.IndexNext, val);} + public void indexPrevious(UInt64 val) {put(Field.IndexPrevious, val);} + public void exchangeRate(UInt64 val) {put(Field.ExchangeRate, val);} + public void rootIndex(Hash256 val) {put(Field.RootIndex, val);} + public void owner(AccountID val) {put(Field.Owner, val);} + public void takerPaysCurrency(Hash160 val) {put(Field.TakerPaysCurrency, val);} + public void takerPaysIssuer(Hash160 val) {put(Field.TakerPaysIssuer, val);} + public void takerGetsCurrency(Hash160 val) {put(Field.TakerGetsCurrency, val);} + public void takerGetsIssuer(Hash160 val) {put(Field.TakerGetsIssuer, val);} + public void indexes(Vector256 val) {put(Field.Indexes, val);} + + public boolean hasIndexNext() {return has(UInt64.IndexNext);} + public boolean hasIndexPrevious() {return has(UInt64.IndexPrevious);} + public boolean hasExchangeRate() {return has(UInt64.ExchangeRate);} + public boolean hasOwner() {return has(AccountID.Owner);} + public boolean hasTakerPaysCurrency() {return has(Hash160.TakerPaysCurrency);} + public boolean hasTakerPaysIssuer() {return has(Hash160.TakerPaysIssuer);} + public boolean hasTakerGetsCurrency() {return has(Hash160.TakerGetsCurrency);} + public boolean hasTakerGetsIssuer() {return has(Hash160.TakerGetsIssuer);} + + public Hash256 nextIndex() { + return Index.directoryNode(rootIndex(), indexNext()); + } + public Hash256 prevIndex() { + return Index.directoryNode(rootIndex(), indexPrevious()); + } + + public boolean hasPreviousIndex() { + return hasIndexPrevious() && !indexPrevious().isZero(); + } + + public boolean hasNextIndex() { + return hasIndexNext() && !indexNext().isZero(); + } + + public boolean isRootIndex() { + return rootIndex().equals(index()); + } + + public void setExchangeDefaults() { + if (takerGetsCurrency() == null) { + takerGetsCurrency(Currency.XRP); + takerGetsIssuer(AccountID.XRP_ISSUER); + } else if (takerPaysCurrency() == null) { + takerPaysCurrency(Currency.XRP); + takerPaysIssuer(AccountID.XRP_ISSUER); + } + } + + @Override + public void setDefaults() { + super.setDefaults(); + if (exchangeRate() != null) { + setExchangeDefaults(); + } + if (indexes() == null) { + indexes(new Vector256()); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Escrow.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Escrow.java new file mode 100644 index 0000000000..ff822c6757 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Escrow.java @@ -0,0 +1,74 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.Index; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.tx.Transaction; + +import java.util.ArrayList; + +public class Escrow extends IndexedLedgerEntry { + public Escrow() { + super(LedgerEntryType.Escrow); + } + + @Override + public void setDefaults() { + super.setDefaults(); + if (multiParty()) { + if (!has(UInt64.DestinationNode)) { + put(UInt64.DestinationNode, UInt64.ZERO); + } + } + } + + @Override + public ArrayList ownerDirectoryIndexes(Transaction nullableContext) { + ArrayList indexes = super.ownerDirectoryIndexes(nullableContext); + if (multiParty()) { + Hash256 destinationOwnerDir = + Index.ownerDirectory(destination()); + indexes.add(Index.directoryNode( + destinationOwnerDir, destinationNode())); + } + return indexes; + } + + private boolean multiParty() { + return !get(AccountID.Account).equals(destination()); + } + + public AccountID account() {return get(AccountID.Account);} + public AccountID destination() {return get(AccountID.Destination);} + public Amount amount() {return get(Amount.Amount);} + public Blob condition() {return get(Blob.Condition);} + public UInt32 cancelAfter() {return get(UInt32.CancelAfter);} + public UInt32 destinationTag() {return get(UInt32.DestinationTag);} + public UInt32 finishAfter() {return get(UInt32.FinishAfter);} + public UInt32 sourceTag() {return get(UInt32.SourceTag);} + public UInt64 destinationNode() {return get(UInt64.DestinationNode);} + public UInt64 ownerNode() {return get(UInt64.OwnerNode);} + + public void amount(Amount val) { put(Amount.Amount, val);} + public void cancelAfter(UInt32 val) { put(UInt32.CancelAfter, val);} + public void condition(Blob val) { put(Blob.Condition, val);} + public void destination(AccountID val) { put(AccountID.Destination, val);} + public void destinationNode(UInt64 val) { put(UInt64.DestinationNode, val);} + public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);} + public void finishAfter(UInt32 val) { put(UInt32.FinishAfter, val);} + public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);} + public void sourceTag(UInt32 val) { put(UInt32.SourceTag, val);} + + public boolean hasCancelAfter() {return has(UInt32.CancelAfter);} + public boolean hasCondition() {return has(Blob.Condition);} + public boolean hasDestinationNode() {return has(UInt64.DestinationNode);} + public boolean hasDestinationTag() {return has(UInt32.DestinationTag);} + public boolean hasFinishAfter() {return has(UInt32.FinishAfter);} + public boolean hasSourceTag() {return has(UInt32.SourceTag);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/FeeSettings.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/FeeSettings.java new file mode 100644 index 0000000000..ce8fca505d --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/FeeSettings.java @@ -0,0 +1,22 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.LedgerEntry; + +public class FeeSettings extends LedgerEntry { + public FeeSettings() { + super(LedgerEntryType.Amendments); + } + public UInt32 referenceFeeUnits() {return get(UInt32.ReferenceFeeUnits);} + public UInt32 reserveBase() {return get(UInt32.ReserveBase);} + public UInt32 reserveIncrement() {return get(UInt32.ReserveIncrement);} + public UInt64 baseFee() {return get(UInt64.BaseFee);} + + public void baseFee(UInt64 val) { put(UInt64.BaseFee, val);} + public void referenceFeeUnits(UInt32 val) { put(UInt32.ReferenceFeeUnits, val);} + public void reserveBase(UInt32 val) { put(UInt32.ReserveBase, val);} + public void reserveIncrement(UInt32 val) { put(UInt32.ReserveIncrement, val);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IHasOwners.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IHasOwners.java new file mode 100644 index 0000000000..93c15e2f80 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IHasOwners.java @@ -0,0 +1,12 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.types.known.tx.Transaction; + +import java.util.ArrayList; + +public interface IHasOwners { + ArrayList ownerDirectoryIndexes(Transaction nullableContext); + AccountID account(Transaction nullableContext); +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IndexedLedgerEntry.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IndexedLedgerEntry.java new file mode 100644 index 0000000000..37ff656961 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IndexedLedgerEntry.java @@ -0,0 +1,56 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.Index; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.ThreadedLedgerEntry; +import com.ripple.core.types.known.tx.Transaction; + +import java.util.ArrayList; + +public abstract class IndexedLedgerEntry extends ThreadedLedgerEntry implements IHasOwners { + public IndexedLedgerEntry(LedgerEntryType type) { + super(type); + } + + @Override + public void setDefaults() { + super.setDefaults(); + if (!has(Field.OwnerNode)) { + put(UInt64.OwnerNode, UInt64.ZERO); + } + } + + @Override + public AccountID account(Transaction nullableContext) { + AccountID account = account(); + if (account == null) { + if (nullableContext != null) { + account = nullableContext.account(); + } else { + throw new IllegalStateException("Cant determine account for: " + prettyJSON()); + } + } + return account; + } + + @Override + public ArrayList ownerDirectoryIndexes(Transaction nullableContext) { + Hash256 ownerDir = Index.ownerDirectory(account(nullableContext)); + ArrayList indexes = new ArrayList<>(); + indexes.add(Index.directoryNode(ownerDir, ownerNode())); + return indexes; + } + + private UInt64 ownerNode() { + return get(UInt64.OwnerNode); + } + + private AccountID account() { + return get(AccountID.Account); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Offer.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Offer.java new file mode 100644 index 0000000000..ceaa7a6732 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Offer.java @@ -0,0 +1,219 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.*; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.Index; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.ThreadedLedgerEntry; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; + +public class Offer extends ThreadedLedgerEntry { + public Offer() { + super(LedgerEntryType.Offer); + } + + /** + * Use the BookDirectory field + * + * @return how much must `pay` to `get` one. + * + */ + public BigDecimal directoryAskQuality() { + return Quality.fromBookDirectory(bookDirectory(), + takerPays().isNative(), + takerGets().isNative()); + } + + /** + * @return how much must `pay` to `get` one. + */ + public BigDecimal askQuality() { + return takerPays().computeQuality(takerGets()); + } + + /** + * @return how much `get` if `pay` one. + */ + public BigDecimal bidQuality() { + return takerGets().computeQuality(takerPays()); + } + + /** + * + * @return One of TakerGets issue, eg 1/USD/BITSTAMP or 1/EUR/SNAPSWAP + */ + public Amount getsOne() { + return takerGets().one(); + } + /** + * + * @return One of TakerPays issue, eg 1/USD/BITSTAMP or 1/EUR/SNAPSWAP + */ + public Amount paysOne() { + return takerPays().one(); + } + + public String getPayCurrencyPair() { + return takerGets().currencyString() + "/" + + takerPays().currencyString(); + } + + // TODO: create an OfferExecution object for this + public STObject executed(STObject finalFields) { + // where `this` is an AffectedNode nodeAsPrevious + STObject executed = new STObject(); + executed.put(Amount.TakerPays, finalFields.get(Amount.TakerPays).subtract(takerPays())); + executed.put(Amount.TakerGets, finalFields.get(Amount.TakerGets).subtract(takerGets())); + return executed; + } + + public Hash256 lineIndex(Amount amt) { + return account().lineIndex(amt.issue()); + } + + public Hash256 fundingSource() { + Amount takerGets = takerGets(); + if (account().equals(takerGets.issuer())) { + return null; + } + else if (takerGets.isNative()) { + return Index.accountRoot(account()); + } else { + return lineIndex(takerGets); + } + } + + public Vector256 lineIndexes() { + Vector256 ret = new Vector256(); + + Amount takerGets = takerGets(); + for (Amount amt : new Amount[]{takerGets, takerPays()}) { + + // Actually want to compare by reference here :) + if (amt == takerGets()) { + // selling own funds + continue; + } + if (!amt.isNative()) { + ret.add(lineIndex(amt)); + } + } + return ret; + } + + public Hash256 bookBase() { + return Index.bookStart(takerPays().issue(), takerGets().issue()); + } + + public boolean belongsToBook(Hash256 bookBase) { + byte[] baseBytes = bookBase.bytes(); + byte[] directoryBytes = bookDirectory().bytes(); + + for (int i = 0; i < 24; i++) { + if (baseBytes[i] != directoryBytes[i]) { + return false; + } + } + return true; + } + + public boolean sellingOwnFunds() { + return account().equals(takerGets().issuer()); + } + + public Amount takerGetsFunded() { + return has(Field.taker_gets_funded) ? get(Amount.taker_gets_funded) : takerGets(); + } + public Amount takerPaysFunded() { + return has(Field.taker_pays_funded) ? get(Amount.taker_pays_funded) : takerPays(); + } + + public static Comparator qualityAscending = Comparator.comparing(Offer::directoryAskQuality); + + public static Iterator iterateCollection(Collection offers) { + final Iterator iterator = offers.iterator(); + + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Offer next() { + return (Offer) iterator.next(); + } + + @Override + public void remove() { + iterator.remove(); + + } + }; + } + + public Hash256 bookNodeDirectoryIndex() { + return Index.directoryNode(bookDirectory(), bookNode()); + } + + public Hash256 ownerNodeDirectoryIndex() { + Hash256 ownerDir = Index.ownerDirectory(account()); + return Index.directoryNode(ownerDir, ownerNode()); + } + + + public UInt32 sequence() {return get(UInt32.Sequence);} + public UInt32 expiration() {return get(UInt32.Expiration);} + public UInt64 bookNode() {return get(UInt64.BookNode);} + public UInt64 ownerNode() {return get(UInt64.OwnerNode);} + public Hash256 bookDirectory() {return get(Hash256.BookDirectory);} + public Amount takerPays() {return get(Amount.TakerPays);} + public Amount takerGets() {return get(Amount.TakerGets);} + public AccountID account() {return get(AccountID.Account);} + public void sequence(UInt32 val) {put(Field.Sequence, val);} + public void expiration(UInt32 val) {put(Field.Expiration, val);} + public void bookNode(UInt64 val) {put(Field.BookNode, val);} + public void ownerNode(UInt64 val) {put(Field.OwnerNode, val);} + public void bookDirectory(Hash256 val) {put(Field.BookDirectory, val);} + public void takerPays(Amount val) {put(Field.TakerPays, val);} + public void takerGets(Amount val) {put(Field.TakerGets, val);} + public void account(AccountID val) {put(Field.Account, val);} + + public boolean hasExpiration() {return has(UInt32.Expiration);} + + public Hash256[] directoryIndexes() { + return new Hash256[]{bookNodeDirectoryIndex(), ownerNodeDirectoryIndex()}; + } + + @Override + public void setDefaults() { + super.setDefaults(); + setOfferDefaults(); + } + + public void setOfferDefaults() { + if (bookNode() == null) { + bookNode(UInt64.ZERO); + } + if (ownerNode() == null) { + ownerNode(UInt64.ZERO); + } + } + + public IssuePair issuePair() { + return new IssuePair(takerPays().issue(), takerGets().issue()); + } + + public Amount payToGet(Amount funded) { + BigDecimal quality = directoryAskQuality(); + // Multiply by one as that will do the rounding operation we want + return paysOne().multiply(funded.multiply(quality)); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OfferDirectory.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OfferDirectory.java new file mode 100644 index 0000000000..fddb7126fd --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OfferDirectory.java @@ -0,0 +1,20 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.Issue; +import com.ripple.core.coretypes.IssuePair; + +public class OfferDirectory extends DirectoryNode { + public IssuePair issuePair() { + return new IssuePair(takerPaysIssue(), takerGetsIssue()); + } + + public Issue takerGetsIssue() { + // TODO: remove wrapper + return Issue.from160s(takerGetsCurrency(), takerGetsIssuer()); + } + public Issue takerPaysIssue() { + // TODO: remove wrapper + return Issue.from160s(takerPaysCurrency(), takerPaysIssuer()); + } + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OwnerDirectory.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OwnerDirectory.java new file mode 100644 index 0000000000..b6f784e4b8 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OwnerDirectory.java @@ -0,0 +1,3 @@ +package com.ripple.core.types.known.sle.entries; + +public class OwnerDirectory extends DirectoryNode { } diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/PayChannel.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/PayChannel.java new file mode 100644 index 0000000000..97a2786b2a --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/PayChannel.java @@ -0,0 +1,42 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.serialized.enums.LedgerEntryType; + +public class PayChannel extends IndexedLedgerEntry { + public PayChannel() { + super(LedgerEntryType.PayChannel); + } + + public AccountID account() {return get(AccountID.Account);} + public AccountID destination() {return get(AccountID.Destination);} + public Amount amount() {return get(Amount.Amount);} + public Amount balance() {return get(Amount.Balance);} + public Blob publicKey() {return get(Blob.PublicKey);} + public UInt32 cancelAfter() {return get(UInt32.CancelAfter);} + public UInt32 destinationTag() {return get(UInt32.DestinationTag);} + public UInt32 expiration() {return get(UInt32.Expiration);} + public UInt32 settleDelay() {return get(UInt32.SettleDelay);} + public UInt32 sourceTag() {return get(UInt32.SourceTag);} + public UInt64 ownerNode() {return get(UInt64.OwnerNode);} + + public void amount(Amount val) { put(Amount.Amount, val);} + public void balance(Amount val) { put(Amount.Balance, val);} + public void cancelAfter(UInt32 val) { put(UInt32.CancelAfter, val);} + public void destination(AccountID val) { put(AccountID.Destination, val);} + public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);} + public void expiration(UInt32 val) { put(UInt32.Expiration, val);} + public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);} + public void publicKey(Blob val) { put(Blob.PublicKey, val);} + public void settleDelay(UInt32 val) { put(UInt32.SettleDelay, val);} + public void sourceTag(UInt32 val) { put(UInt32.SourceTag, val);} + + public boolean hasCancelAfter() {return has(UInt32.CancelAfter);} + public boolean hasDestinationTag() {return has(UInt32.DestinationTag);} + public boolean hasExpiration() {return has(UInt32.Expiration);} + public boolean hasSourceTag() {return has(UInt32.SourceTag);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/RippleState.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/RippleState.java new file mode 100644 index 0000000000..02f9f2ad2e --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/RippleState.java @@ -0,0 +1,224 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Currency; +import com.ripple.core.coretypes.Issue; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.Index; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.enums.LedgerFlag; +import com.ripple.core.fields.AmountField; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.ThreadedLedgerEntry; + +import java.util.Arrays; +import java.util.List; + +public class RippleState extends ThreadedLedgerEntry { + /** + The RippleState is a ledger entry which roughly speaking defines the balance and + trust limits between two accounts. + + Like all current ledger entries, it has a canonical form for hashing that + doesn't necessarily communicate the information in a way that is clear or + obvious to a human. The current json format for some ledger entries is very + close to the hashing format, and isn't any better. + + The two accounts on the link are categorized into a low account, and a high + account by comparing the 160bits of their account id as a big endian unsigned + integer. + + There is one and only one `Balance` stored, using an amount struct with a + neutral `issuer`. ( A uint160 with the numerical value of `1` was chosen as a + placeholder, as the canonical way to represent a null account ) + + The Balance can be negative, zero, or positive and is in terms of the Low + account, such that when it's positive, it defines how much credit the High + account has issued. + + Between any account, there can be two types of balance changes, issuance and + redemption. A redemption is the transferal of previously issued IOUs back to the + owner. + + This implies that for any account, that can they can make a transferal using + funds from two distinct balances. After making this distinction it becomes + nonsensical to say that an account holds negative IOUs from the opposite line. + + Thus, the one Balance in a ripple state actually implies 4 distinct balances. + + ``` + { + ... + "Balance" : { + "currency" : "USD", + "issuer" : "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value" : "100" + }, + "HighLimit" : { + "currency" : "USD", + "issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", + "value" : "500" + }, + "LowLimit" : { + "currency" : "USD", + "issuer" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "value" : "500" + }, + } + ``` + + lowAccount = LowLimit.issuer + hiAccount = HighLimit.issuer + + Balances: + lowAccount has 100/USD/highAccount + lowAccount has 0/USD/lowAccount on the line + + highAccount has 0/USD/lowAccount + highAccount has -100/USD/highAccount on the line + + + * */ + + public RippleState() { + super(LedgerEntryType.RippleState); + } + + public UInt32 highQualityIn() {return get(UInt32.HighQualityIn);} + public UInt32 highQualityOut() {return get(UInt32.HighQualityOut);} + public UInt32 lowQualityIn() {return get(UInt32.LowQualityIn);} + public UInt32 lowQualityOut() {return get(UInt32.LowQualityOut);} + public UInt64 lowNode() {return get(UInt64.LowNode);} + public UInt64 highNode() {return get(UInt64.HighNode);} + public Amount balance() {return get(Amount.Balance);} + public Amount lowLimit() {return get(Amount.LowLimit);} + public Amount highLimit() {return get(Amount.HighLimit);} + public void highQualityIn(UInt32 val) {put(Field.HighQualityIn, val);} + public void highQualityOut(UInt32 val) {put(Field.HighQualityOut, val);} + public void lowQualityIn(UInt32 val) {put(Field.LowQualityIn, val);} + public void lowQualityOut(UInt32 val) {put(Field.LowQualityOut, val);} + public void lowNode(UInt64 val) {put(Field.LowNode, val);} + public void highNode(UInt64 val) {put(Field.HighNode, val);} + public void balance(Amount val) {put(Field.Balance, val);} + public void lowLimit(Amount val) {put(Field.LowLimit, val);} + public void highLimit(Amount val) {put(Field.HighLimit, val);} + + public boolean hasHighQualityIn() {return has(UInt32.HighQualityIn);} + public boolean hasHighQualityOut() {return has(UInt32.HighQualityOut);} + public boolean hasLowQualityIn() {return has(UInt32.LowQualityIn);} + public boolean hasLowQualityOut() {return has(UInt32.LowQualityOut);} + public boolean hasLowNode() {return has(UInt64.LowNode);} + public boolean hasHighNode() {return has(UInt64.HighNode);} + + public AccountID lowAccount() { + return lowLimit().issuer(); + } + + public AccountID highAccount() { + return highLimit().issuer(); + } + + public List sortedAccounts() { + return Arrays.asList(lowAccount(), highAccount()); + } + + public AmountField limitFieldFor(AccountID source) { + if (lowAccount().equals(source)) { + return Amount.LowLimit; + } + if (highAccount().equals(source)) { + return Amount.HighLimit; + } else { + return null; + } + } + + public boolean isFor(AccountID source) { + return lowAccount().equals(source) || highAccount().equals(source); + } + + public boolean isFor(Issue issue) { + return isFor(issue.issuer()) && balance().currency().equals(issue.currency()); + } + + // TODO, can optimize this + public boolean isFor(AccountID s1, AccountID s2, Currency currency) { + return currency.equals(balance().currency()) && isFor(s1) && isFor(s2); + } + + public Currency currency() { + return balance().currency(); + } + + private Amount issuedBy(boolean hi) { + Amount balance; + + if (hi) { + balance = balance().newIssuer(highAccount()); + } else { + balance = balance().negate().newIssuer(lowAccount()); + } + + if (!balance.isPositive()) { + balance = balance.issue().amount(0); + } + return balance; + } + + public Amount issuedByHigh() { + return issuedBy(true); + } + public Amount issuedByLow() { + return issuedBy(false); + } + + public Amount issuedTo(AccountID accountID) { + return issuedBy(isLowAccount(accountID)); + } + + @Deprecated() // "not deprecated but needs fixing" + public boolean authorizedBy(AccountID account) { + UInt32 flags = flags(); + return flags == null || flags.testBit(isHighAccount(account) ? LedgerFlag.HighAuth : LedgerFlag.LowAuth); + } + + private boolean isBitSet(int flags, int flag) { + return (flags & flag) != 0; + } + + private boolean isHighAccount(AccountID account) { + return highAccount().equals(account); + } + private boolean isLowAccount(AccountID account) { + return lowAccount().equals(account); + } + + + public Hash256 lowNodeOwnerDirectory() { + Hash256 ownerDir = Index.ownerDirectory(lowAccount()); + return Index.directoryNode(ownerDir, lowNode()); + } + public Hash256 highNodeOwnerDirectory() { + Hash256 ownerDir = Index.ownerDirectory(highAccount()); + return Index.directoryNode(ownerDir, highNode()); + } + + public Hash256[] directoryIndexes() { + return new Hash256[]{lowNodeOwnerDirectory(), highNodeOwnerDirectory()}; + } + + @Override + public void setDefaults() { + super.setDefaults(); + + if (lowNode() == null) { + lowNode(UInt64.ZERO); + } + if (highNode() == null) { + highNode(UInt64.ZERO); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/SignerList.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/SignerList.java new file mode 100644 index 0000000000..68af847605 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/SignerList.java @@ -0,0 +1,32 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.STArray; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.LedgerEntryType; + +public class SignerList extends IndexedLedgerEntry { + public SignerList() { + super(LedgerEntryType.SignerList); + } + + @Override + public void setDefaults() { + super.setDefaults(); + if (!has(Field.SignerListID)) { + put(UInt32.SignerListID, UInt32.ZERO); + } + } + + public STArray signerEntries() {return get(STArray.SignerEntries);} + public UInt32 signerListID() {return get(UInt32.SignerListID);} + public UInt32 signerQuorum() {return get(UInt32.SignerQuorum);} + public UInt64 ownerNode() {return get(UInt64.OwnerNode);} + + public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);} + public void signerEntries(STArray val) { put(STArray.SignerEntries, val);} + public void signerListID(UInt32 val) { put(UInt32.SignerListID, val);} + public void signerQuorum(UInt32 val) { put(UInt32.SignerQuorum, val);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Ticket.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Ticket.java new file mode 100644 index 0000000000..5d48e3f462 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Ticket.java @@ -0,0 +1,27 @@ +package com.ripple.core.types.known.sle.entries; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.serialized.enums.LedgerEntryType; + +public class Ticket extends IndexedLedgerEntry { + public Ticket() { + super(LedgerEntryType.Ticket); + } + + public AccountID account() {return get(AccountID.Account);} + public AccountID target() {return get(AccountID.Target);} + public UInt32 expiration() {return get(UInt32.Expiration);} + public UInt32 sequence() {return get(UInt32.Sequence);} + public UInt64 ownerNode() {return get(UInt64.OwnerNode);} + + public void expiration(UInt32 val) { put(UInt32.Expiration, val);} + public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);} + public void sequence(UInt32 val) { put(UInt32.Sequence, val);} + public void target(AccountID val) { put(AccountID.Target, val);} + + public boolean hasExpiration() {return has(UInt32.Expiration);} + public boolean hasTarget() {return has(AccountID.Target);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/Transaction.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/Transaction.java new file mode 100644 index 0000000000..8f174968aa --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/Transaction.java @@ -0,0 +1,116 @@ +package com.ripple.core.types.known.tx; + +import com.ripple.core.coretypes.*; +import com.ripple.core.coretypes.hash.HalfSha512; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.uint.UInt16; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.enums.TransactionFlag; +import com.ripple.core.fields.Field; +import com.ripple.core.formats.TxFormat; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.signed.SignedTransaction; +import com.ripple.crypto.keys.IVerifyingKey; +import com.ripple.utils.HashUtils; + +public class Transaction extends STObject { + public static final boolean CANONICAL_FLAG_DEPLOYED = true; + public static final UInt32 CANONICAL_SIGNATURE = new UInt32(TransactionFlag.FullyCanonicalSig); + + public Transaction(TransactionType type) { + setFormat(TxFormat.formats.get(type)); + put(Field.TransactionType, type); + } + + public SignedTransaction prepare(byte[] pubKeyBytes) { + SignedTransaction tx = SignedTransaction.fromTx(this); + tx.prepare(pubKeyBytes); + return tx; + } + + public TransactionType transactionType() { + return transactionType(this); + } + + public Hash256 signingHash() { + return signingHash(HashPrefix.txSign); + } + + public byte[] signingData() { + return signingData(HashPrefix.txSign); + } + + public boolean verifySignature(AccountID key) { + Blob pubKey = signingPubKey(); + return IVerifyingKey.from(pubKey.toBytes()) + .verify(signingData(), + txnSignature().toBytes()) && + signingKey().equals(key); + } + + public boolean verifyMasterKeySignature() { + return verifySignature(account()); + } + + public void setCanonicalSignatureFlag() { + UInt32 flags = get(UInt32.Flags); + if (flags == null) { + flags = CANONICAL_SIGNATURE; + } else { + flags = flags.or(CANONICAL_SIGNATURE); + } + put(UInt32.Flags, flags); + } + + public UInt32 flags() {return get(UInt32.Flags);} + public UInt32 sourceTag() {return get(UInt32.SourceTag);} + public UInt32 sequence() {return get(UInt32.Sequence);} + public UInt32 lastLedgerSequence() {return get(UInt32.LastLedgerSequence);} + public UInt32 operationLimit() {return get(UInt32.OperationLimit);} + public Hash256 previousTxnID() {return get(Hash256.PreviousTxnID);} + public Hash256 accountTxnID() {return get(Hash256.AccountTxnID);} + public Amount fee() {return get(Amount.Fee);} + public Blob signingPubKey() {return get(Blob.SigningPubKey);} + public Blob txnSignature() {return get(Blob.TxnSignature);} + public AccountID account() {return get(AccountID.Account);} + + public void transactionType(UInt16 val) {put(Field.TransactionType, val);} + public void flags(UInt32 val) {put(Field.Flags, val);} + public void sourceTag(UInt32 val) {put(Field.SourceTag, val);} + public void sequence(UInt32 val) {put(Field.Sequence, val);} + public void lastLedgerSequence(UInt32 val) {put(Field.LastLedgerSequence, val);} + public void operationLimit(UInt32 val) {put(Field.OperationLimit, val);} + public void previousTxnID(Hash256 val) {put(Field.PreviousTxnID, val);} + public void accountTxnID(Hash256 val) {put(Field.AccountTxnID, val);} + public void fee(Amount val) {put(Field.Fee, val);} + public void signingPubKey(Blob val) {put(Field.SigningPubKey, val);} + public void txnSignature(Blob val) {put(Field.TxnSignature, val);} + public void account(AccountID val) {put(Field.Account, val);} + + public boolean hasFlags() {return has(UInt32.Flags);} + public boolean hasSourceTag() {return has(UInt32.SourceTag);} + public boolean hasLastLedgerSequence() {return has(UInt32.LastLedgerSequence);} + public boolean hasOperationLimit() {return has(UInt32.OperationLimit);} + public boolean hasPreviousTxnID() {return has(Hash256.PreviousTxnID);} + public boolean hasAccountTxnID() {return has(Hash256.AccountTxnID);} + public boolean hasTxnSignature() {return has(Blob.TxnSignature);} + public boolean hasSigners() {return has(STArray.Signers);} + public boolean hasMemos() {return has(STArray.Memos);} + + public Hash256 hash() { + return get(Hash256.hash); + } + + public Hash256 createHash() { + HalfSha512 id = HalfSha512.prefixed256(HashPrefix.transactionID); + toBytesSink(id); + return id.finish(); + } + + public AccountID signingKey() { + // May be a regular Key + byte[] pubKey = HashUtils.SHA256_RIPEMD160(signingPubKey().toBytes()); + return AccountID.fromBytes(pubKey); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/AffectedNode.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/AffectedNode.java new file mode 100644 index 0000000000..9c399b73a0 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/AffectedNode.java @@ -0,0 +1,156 @@ +package com.ripple.core.types.known.tx.result; + +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt16; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.SerializedType; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.LedgerEntry; + +// TODO: fix up this nonsense +public class AffectedNode extends STObject { + public final Field field; + public final STObject nested; + + public AffectedNode(STObject source) { + fields = source.getFields(); + field = getField(); + nested = nestedObject(); + } + + public boolean isOffer() { + return ledgerEntryType() == LedgerEntryType.Offer; + } + + public boolean isAccountRoot() { + return ledgerEntryType() == LedgerEntryType.AccountRoot; + } + + public boolean isRippleState() { + return ledgerEntryType() == LedgerEntryType.RippleState; + } + + public boolean isDirectoryNode() { + return ledgerEntryType() == LedgerEntryType.DirectoryNode; + } + + public boolean wasPreviousNode() { + return isDeletedNode() || isModifiedNode(); + } + + public boolean isFinalNode() { + return true; + } + + public boolean isCreatedNode() { + return field == Field.CreatedNode; + } + + public boolean isDeletedNode() { + return field == Field.DeletedNode; + } + + public boolean isModifiedNode() { + return field == Field.ModifiedNode; + } + + public Field getField() { +// return iterator().next(); + return fields.firstKey(); + } + + public Hash256 ledgerIndex() { + return nested.get(Hash256.LedgerIndex); + } + + public LedgerEntryType ledgerEntryType() { + return ledgerEntryType(nested); + } + + private STObject nestedObject() { + return (STObject) get(getField()); + } + + /** + * @return - LedgerEntry before the transaction (or after in the case of + * a CreatedNode) + */ + public LedgerEntry nodeAsPrevious() { + return (LedgerEntry) rebuildFromMeta(true); + } + + public LedgerEntry nodeAsFinal() { + return (LedgerEntry) rebuildFromMeta(false); + } + + private STObject rebuildFromMeta(boolean asPrevious) { + boolean created = isCreatedNode(); + STObject mixed = new STObject(); + + // The first object has only a single key + Field wrapperField = created ? Field.CreatedNode : + isDeletedNode() ? Field.DeletedNode : + Field.ModifiedNode; + + STObject wrapped = (STObject) get(wrapperField); + + Field finalFields = created ? Field.NewFields : + Field.FinalFields; + + // You may get some AccountRoot objects like this + if (!wrapped.has(finalFields)) { + STObject source = new STObject(wrapped.getFields()); + source.put(Hash256.index, wrapped.get(Hash256.LedgerIndex)); + source.remove(Field.LedgerIndex); + return STObject.formatted(source); + } + + // Get all the final fields + STObject finals = (STObject) wrapped.get(finalFields); + for (Field field : finals) { + mixed.put(field, finals.get(field)); + } + + // Then layer over the previous fields if desired as previous + // DirectoryNode LedgerEntryType won't have `PreviousFields` + if (asPrevious && wrapped.has(Field.PreviousFields)) { + STObject previous = wrapped.get(STObject.PreviousFields); + for (Field field : previous) { + mixed.put(field, previous.get(field)); + } + } + + // Keep the inner most fields + for (Field field : wrapped) { + switch (field) { + case NewFields: + case PreviousFields: + case FinalFields: + continue; + default: + SerializedType value = wrapped.get(field); + if (field == Field.LedgerIndex) { + field = Field.index; + } + mixed.put(field, value); + + } + } + return STObject.formatted(mixed); + } + + public static boolean isAffectedNode(STObject source) { + return (source.size() == 1 && ( + source.has(DeletedNode) || + source.has(CreatedNode) || + source.has(ModifiedNode))); + } + + public boolean removedField(Field field) { + return nested.has(Field.PreviousFields) && + nested.get(STObject.PreviousFields).has(field) && + nested.has(Field.FinalFields) && + !nested.get(STObject.FinalFields).has(field); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionMeta.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionMeta.java new file mode 100644 index 0000000000..c25183436a --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionMeta.java @@ -0,0 +1,50 @@ +package com.ripple.core.types.known.tx.result; + +import com.ripple.core.coretypes.STArray; +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt8; +import com.ripple.core.serialized.enums.EngineResult; +import com.ripple.core.types.known.sle.LedgerEntry; + +import java.util.Iterator; + +public class TransactionMeta extends STObject { + public static boolean isTransactionMeta(STObject source) { + return source.has(UInt8.TransactionResult) && + source.has(STArray.AffectedNodes); + } + + public EngineResult engineResult() { + return engineResult(this); + } + + public Iterable affectedNodes() { + STArray nodes = get(STArray.AffectedNodes); + final Iterator iterator = nodes.iterator(); + return () -> iterateAffectedNodes(iterator); + } + + private static Iterator iterateAffectedNodes(final Iterator iterator) { + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public AffectedNode next() { + return (AffectedNode) iterator.next(); + } + + @Override + public void remove() { + iterator.remove(); + } + }; + } + + public UInt32 transactionIndex() { + return get(UInt32.TransactionIndex); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionResult.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionResult.java new file mode 100644 index 0000000000..2e4467bf14 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionResult.java @@ -0,0 +1,299 @@ +package com.ripple.core.types.known.tx.result; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.STArray; +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.Index; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.EngineResult; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.sle.entries.AccountRoot; +import com.ripple.core.types.known.tx.Transaction; +import com.ripple.encodings.common.B16; +import org.json.JSONObject; + +import java.util.Map; +import java.util.TreeMap; + +public class TransactionResult implements Comparable { + // The json formatting of transaction results is a MESS + public enum Source { + request_tx_result, + request_account_tx, + request_account_tx_binary, + request_tx_binary, + ledger_transactions_expanded_with_ledger_index_injected, + transaction_subscription_notification + } + + public EngineResult engineResult; + public UInt32 ledgerIndex; + public Hash256 hash; + + // TODO: in practice this class is GENERALLY only for validated results. + // NOT strictly true as you can get results from closed but not validated + // ledgers! Though you would want to beyond testing ... + public boolean validated; + + public TransactionResult(long ledgerIndex, Hash256 hash, Transaction txn, TransactionMeta meta) { + this.ledgerIndex = new UInt32(ledgerIndex); + this.hash = hash; + this.txn = txn; + this.meta = meta; + this.engineResult = meta.engineResult(); + this.validated = true; + } + + public Transaction txn; + public TransactionMeta meta; + + public TransactionType transactionType() { + return txn.transactionType(); + } + + public AccountID createdAccount() { + AccountID destination = null; + Hash256 destinationIndex = null; + + if (transactionType() == TransactionType.Payment && meta.has(Field.AffectedNodes)) { + STArray affected = meta.get(STArray.AffectedNodes); + for (STObject node : affected) { + if (node.has(STObject.CreatedNode)) { + STObject created = node.get(STObject.CreatedNode); + if (STObject.ledgerEntryType(created) == LedgerEntryType.AccountRoot) { + if (destination == null) { + destination = txn.get(AccountID.Destination); + destinationIndex = Index.accountRoot(destination); + } + if (destinationIndex.equals(created.get(Hash256.LedgerIndex))) { + return destination; + } + } + } + } + } + return null; + } + + public Map modifiedRoots() { + TreeMap accounts = null; + + if (meta.has(Field.AffectedNodes)) { + accounts = new TreeMap<>(); + for (AffectedNode fields : meta.affectedNodes()) { + if (fields.isModifiedNode() && fields.isAccountRoot()) { + AccountRoot root = (AccountRoot) fields.nodeAsFinal(); + //noinspection StatementWithEmptyBody + if (root.account() != null) { + accounts.put(root.account(), root); + } else { + // TODO: Remember why these modified nodes have no + // FinalFields or NewFields + /* + {"ModifiedNode": { + "LedgerIndex": "2C6F7594FB7471F4983C2BC691AAC2F25F8DB88D455985B4181E053D7AB23006", + "PreviousTxnLgrSeq": 35561097, + "LedgerEntryType": "AccountRoot", + "index": "2C6F7594FB7471F4983C2BC691AAC2F25F8DB88D455985B4181E053D7AB23006", + "PreviousTxnID": "67EE84E892FBEDF5FD52D511FF1E833870A2B8104CA5FF9BA89867A528A5D3ED" + }} + * */ + } + } + } + } + return accounts; + } + + public AccountID initiatingAccount() { + return txn.get(AccountID.Account); + } + + public int compareTo(TransactionResult o2) { + TransactionResult o1 = this; + int i = o1.ledgerIndex.compareTo(o2.ledgerIndex); + if (i != 0) { + return i; + } else { + return o1.meta.transactionIndex() + .compareTo(o2.meta.transactionIndex()); + } + } + + public static TransactionResult fromJSON(ObjectNode json) { + // TODO: obviously ... + return fromJSON(new JSONObject(json.toString())); + } + + public static TransactionResult fromJSON(JSONObject json) { + boolean binary; + + String metaKey = json.has("meta") ? "meta" : "metaData"; + + String txKey = json.has("transaction") ? "transaction" : + json.has("tx") ? "tx" : + json.has("tx_blob") ? "tx_blob" : null; + + if (txKey == null && !json.has("TransactionType")) { + throw new IllegalArgumentException("This json isn't a transaction " + json); + } + + binary = txKey != null && json.get(txKey) instanceof String; + + Transaction txn; + if (txKey == null) { + // This should parse the `hash` field + txn = (Transaction) STObject.fromJSONObject(json); + } else { + txn = (Transaction) parseObject(json, txKey, binary); + if (json.has("hash")) { + txn.put(Hash256.hash, Hash256.fromHex(json.getString("hash"))); + } else if (binary) { + byte[] decode = B16.decode(json.getString(txKey)); + txn.put(Hash256.hash, Index.transactionID(decode)); + } + } + + TransactionMeta meta = (TransactionMeta) parseObject(json, metaKey, binary); + long ledger_index = json.optLong("ledger_index", 0); + if (ledger_index == 0 && !binary) { + ledger_index = json.getJSONObject(txKey).getLong("ledger_index"); + } + + TransactionResult tr = new TransactionResult(ledger_index, txn.get(Hash256.hash), txn, meta); +// if (json.has("ledger_hash")) { +// tr.ledgerHash = Hash256.fromHex(json.getString("ledger_hash")); +// } + return tr; + } + + private static STObject parseObject(JSONObject json, String key, boolean binary) { + if (binary) { + return STObject.fromHex(json.getString(key)); + } else { + JSONObject tx_json = json.getJSONObject(key); + return STObject.fromJSONObject(tx_json); + } + } + + public TransactionResult(JSONObject json, Source resultMessageSource) { + if (resultMessageSource == Source.transaction_subscription_notification) { + + engineResult = EngineResult.valueOf(json.getString("engine_result")); + validated = json.getBoolean("validated"); +// ledgerHash = Hash256.fromHex(json.getString("ledger_hash")); + ledgerIndex = new UInt32(json.getLong("ledger_index")); + + if (json.has("transaction")) { + txn = (Transaction) STObject.fromJSONObject(json.getJSONObject("transaction")); + hash = txn.get(Hash256.hash); + } + + if (json.has("meta")) { + meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("meta")); + } + } + else if (resultMessageSource == Source.ledger_transactions_expanded_with_ledger_index_injected) { + validated = true; + meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("metaData")); + txn = (Transaction) STObject.fromJSONObject(json); + hash = txn.get(Hash256.hash); + engineResult = meta.engineResult(); + ledgerIndex = new UInt32(json.getLong("ledger_index")); + + } else if (resultMessageSource == Source.request_tx_result) { + validated = json.optBoolean("validated", false); + if (validated && !json.has("meta")) { + throw new IllegalStateException("It's validated, why doesn't it have meta??"); + } + if (validated) { + meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("meta")); + engineResult = meta.engineResult(); + txn = (Transaction) STObject.fromJSONObject(json); + hash = txn.get(Hash256.hash); + ledgerIndex = new UInt32(json.getLong("ledger_index")); + + } + } else if (resultMessageSource == Source.request_account_tx) { + validated = json.optBoolean("validated", false); + if (validated && !json.has("meta")) { + throw new IllegalStateException("It's validated, why doesn't it have meta??"); + } + if (validated) { + JSONObject tx = json.getJSONObject("tx"); + meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("meta")); + engineResult = meta.engineResult(); + this.txn = (Transaction) STObject.fromJSONObject(tx); + hash = this.txn.get(Hash256.hash); + ledgerIndex = new UInt32(tx.getLong("ledger_index")); + } + } else if (resultMessageSource == Source.request_account_tx_binary || resultMessageSource == Source.request_tx_binary) { + validated = json.optBoolean("validated", false); + if (validated && !json.has("meta")) { + throw new IllegalStateException("It's validated, why doesn't it have meta??"); + } + if (validated) { + /* + { + "ledger_index": 3378767, + "meta": "201 ...", + "tx_blob": "120 ...", + "validated": true + }, + */ + boolean account_tx = resultMessageSource == Source.request_account_tx_binary; + + String tx = json.getString(account_tx ? "tx_blob" : "tx"); + byte[] decodedTx = B16.decode(tx); + meta = (TransactionMeta) STObject.fromHex(json.getString("meta")); + this.txn = (Transaction) STObject.fromBytes(decodedTx); + + if (account_tx) { + hash = Index.transactionID(decodedTx); + } else { + hash = Hash256.fromHex(json.getString("hash")); + } + this.txn.put(Field.hash, hash); + + engineResult = meta.engineResult(); + ledgerIndex = new UInt32(json.getLong("ledger_index")); + } + } + } + + @Override + public String toString() { + JSONObject object = toJSON(); + return object.toString(); + } + + public JSONObject toJSON() { + JSONObject o = new JSONObject(); + o.put("tx", txn.toJSON()); + o.put("meta", meta.toJSON()); + o.put("ledger_index", ledgerIndex); + o.put("hash", hash.toHex()); + return o; + } + + public TransactionResult copy() { + TransactionMeta metaCopy = (TransactionMeta) STObject.fromBytes(meta.toBytes()); + Transaction txnCopy = (Transaction) STObject.fromBytes(txn.toBytes()); + return new TransactionResult(ledgerIndex.longValue(), hash, txnCopy, metaCopy); + } + + public JSONObject toJSONBinary() { + JSONObject o = new JSONObject(); + + o.put("hash", hash.toHex()); + o.put("meta", meta.toHex()); + o.put("tx", txn.toHex()); + o.put("ledger_index", ledgerIndex); + + return o; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/signed/SignedTransaction.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/signed/SignedTransaction.java new file mode 100644 index 0000000000..56440a9bf1 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/signed/SignedTransaction.java @@ -0,0 +1,94 @@ +package com.ripple.core.types.known.tx.signed; + +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.hash.HalfSha512; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.BytesList; +import com.ripple.core.serialized.MultiSink; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +import java.util.Arrays; + +public class SignedTransaction { + private SignedTransaction(Transaction of) { + // TODO: is this just over kill ? + txn = (Transaction) STObject.fromBytes(of.toBytes()); + } + + protected SignedTransaction() { + } + + public Transaction txn; + public Hash256 hash; + + public byte[] signingData; + public byte[] previousSigningData; + public String tx_blob; + + public static SignedTransaction fromTx(Transaction tx) { + return new SignedTransaction(tx); + } + + public void prepare(byte[] pubKeyBytes) { + prepare(pubKeyBytes,null, null, null); + } + + public void prepare(byte[] pubKeyBytes, + Amount fee, + UInt32 Sequence, + UInt32 lastLedgerSequence) { + + Blob pubKey = new Blob(pubKeyBytes); + + // This won't always be specified + if (lastLedgerSequence != null) { + txn.put(UInt32.LastLedgerSequence, lastLedgerSequence); + } + if (Sequence != null) { + txn.put(UInt32.Sequence, Sequence); + } + if (fee != null) { + txn.put(Amount.Fee, fee); + } + + txn.signingPubKey(pubKey); + + if (Transaction.CANONICAL_FLAG_DEPLOYED) { + txn.setCanonicalSignatureFlag(); + } + + txn.checkFormat(); + signingData = txn.signingData(); + if (previousSigningData != null && Arrays.equals(signingData, previousSigningData)) { + return; + } + } + + public void addSign(byte[] signature) { + try { + txn.txnSignature(new Blob(signature)); + + BytesList blob = new BytesList(); + HalfSha512 id = HalfSha512.prefixed256(HashPrefix.transactionID); + + txn.toBytesSink(new MultiSink(blob, id)); + tx_blob = blob.bytesHex(); + hash = id.finish(); + } catch (Exception e) { + // electric paranoia + previousSigningData = null; + throw new RuntimeException(e); + } /*else {*/ + previousSigningData = signingData; + // } + } + + public TransactionType transactionType() { + return txn.transactionType(); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/AccountSet.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/AccountSet.java new file mode 100644 index 0000000000..0e8a1c9672 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/AccountSet.java @@ -0,0 +1,46 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.hash.Hash128; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt8; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class AccountSet extends Transaction{ + public AccountSet() { + super(TransactionType.AccountSet); + } + public UInt32 transferRate() {return get(UInt32.TransferRate);} + public UInt32 walletSize() {return get(UInt32.WalletSize);} + public UInt32 setFlag() {return get(UInt32.SetFlag);} + public UInt32 clearFlag() {return get(UInt32.ClearFlag);} + public Hash128 emailHash() {return get(Hash128.EmailHash);} + public Hash256 walletLocator() {return get(Hash256.WalletLocator);} + public Blob messageKey() {return get(Blob.MessageKey);} + public Blob domain() {return get(Blob.Domain);} + public UInt8 tickSize() {return get(UInt8.TickSize);} + + public void transferRate(UInt32 val) {put(Field.TransferRate, val);} + public void walletSize(UInt32 val) {put(Field.WalletSize, val);} + public void setFlag(UInt32 val) {put(Field.SetFlag, val);} + public void clearFlag(UInt32 val) {put(Field.ClearFlag, val);} + public void emailHash(Hash128 val) {put(Field.EmailHash, val);} + public void walletLocator(Hash256 val) {put(Field.WalletLocator, val);} + public void messageKey(Blob val) {put(Field.MessageKey, val);} + public void domain(Blob val) {put(Field.Domain, val);} + public void tickSize(UInt8 val) { put(UInt8.TickSize, val);} + + public boolean hasTransferRate() {return has(UInt32.TransferRate);} + public boolean hasWalletSize() {return has(UInt32.WalletSize);} + public boolean hasSetFlag() {return has(UInt32.SetFlag);} + public boolean hasClearFlag() {return has(UInt32.ClearFlag);} + public boolean hasEmailHash() {return has(Hash128.EmailHash);} + public boolean hasWalletLocator() {return has(Hash256.WalletLocator);} + public boolean hasMessageKey() {return has(Blob.MessageKey);} + public boolean hasDomain() {return has(Blob.Domain);} + public boolean hasTickSize() {return has(UInt8.TickSize);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCancel.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCancel.java new file mode 100644 index 0000000000..f5b961f514 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCancel.java @@ -0,0 +1,14 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class CheckCancel extends Transaction { + public CheckCancel() { + super(TransactionType.CheckCancel); + } + + public Hash256 checkID() {return get(Hash256.CheckID);} + public void checkID(Hash256 val) { put(Hash256.CheckID, val);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCash.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCash.java new file mode 100644 index 0000000000..8eba2458e9 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCash.java @@ -0,0 +1,23 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class CheckCash extends Transaction { + public CheckCash() { + super(TransactionType.CheckCash); + } + + public boolean hasAmount() {return has(Amount.Amount);} + public boolean hasDeliverMin() {return has(Amount.DeliverMin);} + + public Hash256 checkID() {return get(Hash256.CheckID);} + public Amount amount() {return get(Amount.Amount);} + public Amount deliverMin() {return get(Amount.DeliverMin);} + + public void checkID(Hash256 val) { put(Hash256.CheckID, val);} + public void amount(Amount val) { put(Amount.Amount, val);} + public void deliverMin(Amount val) { put(Amount.DeliverMin, val);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCreate.java new file mode 100644 index 0000000000..b8e75a9b0f --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCreate.java @@ -0,0 +1,30 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class CheckCreate extends Transaction { + public CheckCreate() { + super(TransactionType.CheckCreate); + } + + public boolean hasExpiration() {return has(UInt32.Expiration);} + public boolean hasDestinationTag() {return has(UInt32.DestinationTag);} + public boolean hasInvoiceID() {return has(Hash256.InvoiceID);} + + public void expiration(UInt32 val) { put(UInt32.Expiration, val);} + public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);} + public void invoiceID(Hash256 val) { put(Hash256.InvoiceID, val);} + public void sendMax(Amount val) { put(Amount.SendMax, val);} + public void destination(AccountID val) { put(AccountID.Destination, val); } + + public AccountID destination() {return get(AccountID.Destination);} + public UInt32 expiration() {return get(UInt32.Expiration);} + public UInt32 destinationTag() {return get(UInt32.DestinationTag);} + public Hash256 invoiceID() {return get(Hash256.InvoiceID);} + public Amount sendMax() {return get(Amount.SendMax);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/DepositPreauth.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/DepositPreauth.java new file mode 100644 index 0000000000..68f08cde32 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/DepositPreauth.java @@ -0,0 +1,19 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class DepositPreauth extends Transaction { + public DepositPreauth() { + super(TransactionType.DepositPreauth); + } + public boolean hasAuthorize() {return has(AccountID.Authorize);} + public AccountID authorize() {return get(AccountID.Authorize);} + public void authorize(AccountID val) { put(AccountID.Authorize, val);} + + public boolean hasUnauthorize() {return has(AccountID.Unauthorize);} + public AccountID unauthorize() {return get(AccountID.Unauthorize);} + public void unauthorize(AccountID val) { put(AccountID.Unauthorize, val);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCancel.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCancel.java new file mode 100644 index 0000000000..ab435bacda --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCancel.java @@ -0,0 +1,18 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class EscrowCancel extends Transaction { + public EscrowCancel() { + super(TransactionType.EscrowCancel); + } + + public UInt32 offerSequence() {return get(UInt32.OfferSequence);} + public void offerSequence(UInt32 val) { put(UInt32.OfferSequence, val);} + public AccountID owner() {return get(AccountID.Owner);} + public void owner(AccountID val) { put(AccountID.Owner, val);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCreate.java new file mode 100644 index 0000000000..3587a6c191 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCreate.java @@ -0,0 +1,33 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class EscrowCreate extends Transaction { + public EscrowCreate() { + super(TransactionType.EscrowCreate); + } + public boolean hasDestinationTag() {return has(UInt32.DestinationTag);} + public boolean hasCancelAfter() {return has(UInt32.CancelAfter);} + public boolean hasFinishAfter() {return has(UInt32.FinishAfter);} + public boolean hasCondition() {return has(Blob.Condition);} + + public Amount amount() {return get(Amount.Amount);} + public AccountID destination() {return get(AccountID.Destination);} + public UInt32 destinationTag() {return get(UInt32.DestinationTag);} + public UInt32 cancelAfter() {return get(UInt32.CancelAfter);} + public UInt32 finishAfter() {return get(UInt32.FinishAfter);} + public Blob condition() {return get(Blob.Condition);} + + public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);} + public void cancelAfter(UInt32 val) { put(UInt32.CancelAfter, val);} + public void finishAfter(UInt32 val) { put(UInt32.FinishAfter, val);} + public void amount(Amount val) { put(Amount.Amount, val);} + public void condition(Blob val) { put(Blob.Condition, val);} + public void destination(AccountID val) { put(AccountID.Destination, val);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowFinish.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowFinish.java new file mode 100644 index 0000000000..7b552e768d --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowFinish.java @@ -0,0 +1,27 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class EscrowFinish extends Transaction { + public EscrowFinish() { + super(TransactionType.EscrowFinish); + } + + public boolean hasFulfillment() {return has(Blob.Fulfillment);} + public boolean hasCondition() {return has(Blob.Condition);} + + public UInt32 offerSequence() {return get(UInt32.OfferSequence);} + public Blob fulfillment() {return get(Blob.Fulfillment);} + public Blob condition() {return get(Blob.Condition);} + public AccountID owner() {return get(AccountID.Owner);} + + public void offerSequence(UInt32 val) { put(UInt32.OfferSequence, val);} + public void fulfillment(Blob val) { put(Blob.Fulfillment, val);} + public void condition(Blob val) { put(Blob.Condition, val);} + public void owner(AccountID val) { put(AccountID.Owner, val);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCancel.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCancel.java new file mode 100644 index 0000000000..461d6cac09 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCancel.java @@ -0,0 +1,13 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class OfferCancel extends Transaction { + public OfferCancel() { + super(TransactionType.OfferCancel); + } + public UInt32 offerSequence() {return get(UInt32.OfferSequence);} + public void offerSequence(UInt32 val) { put(UInt32.OfferSequence, val);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCreate.java new file mode 100644 index 0000000000..dbb783479d --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCreate.java @@ -0,0 +1,25 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class OfferCreate extends Transaction { + public OfferCreate() { + super(TransactionType.OfferCreate); + } + public UInt32 expiration() {return get(UInt32.Expiration);} + public UInt32 offerSequence() {return get(UInt32.OfferSequence);} + public Amount takerPays() {return get(Amount.TakerPays);} + public Amount takerGets() {return get(Amount.TakerGets);} + public void expiration(UInt32 val) {put(Field.Expiration, val);} + public void offerSequence(UInt32 val) {put(Field.OfferSequence, val);} + public void takerPays(Amount val) {put(Field.TakerPays, val);} + public void takerGets(Amount val) {put(Field.TakerGets, val);} + + public boolean hasExpiration() {return has(UInt32.Expiration);} + public boolean hasOfferSequence() {return has(UInt32.OfferSequence);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/Payment.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/Payment.java new file mode 100644 index 0000000000..10da726d71 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/Payment.java @@ -0,0 +1,39 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.PathSet; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class Payment extends Transaction { + public Payment() { + super(TransactionType.Payment); + } + + public UInt32 destinationTag() {return get(UInt32.DestinationTag);} + public Hash256 invoiceID() {return get(Hash256.InvoiceID);} + public Amount amount() {return get(Amount.Amount);} + public Amount sendMax() {return get(Amount.SendMax);} + public AccountID destination() {return get(AccountID.Destination);} + + public PathSet paths() {return get(PathSet.Paths);} + public Amount deliverMin() {return get(Amount.DeliverMin);} + + public void destinationTag(UInt32 val) {put(Field.DestinationTag, val);} + public void invoiceID(Hash256 val) {put(Field.InvoiceID, val);} + public void deliverMin(Amount val) { put(Amount.DeliverMin, val);} + public void amount(Amount val) {put(Field.Amount, val);} + public void sendMax(Amount val) {put(Field.SendMax, val);} + public void destination(AccountID val) {put(Field.Destination, val);} + public void paths(PathSet val) {put(Field.Paths, val);} + + public boolean hasDestinationTag() {return has(UInt32.DestinationTag);} + public boolean hasInvoiceID() {return has(Hash256.InvoiceID);} + public boolean hasSendMax() {return has(Amount.SendMax);} + public boolean hasDeliverMin() {return has(Amount.DeliverMin);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelClaim.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelClaim.java new file mode 100644 index 0000000000..8632dbd8b3 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelClaim.java @@ -0,0 +1,31 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class PaymentChannelClaim extends Transaction { + public PaymentChannelClaim() { + super(TransactionType.PaymentChannelClaim); + } + + public boolean hasAmount() {return has(Amount.Amount);} + public boolean hasBalance() {return has(Amount.Balance);} + public boolean hasPublicKey() {return has(Blob.PublicKey);} + public boolean hasSignature() {return has(Blob.Signature);} + + public void channel(Hash256 val) { put(Hash256.Channel, val);} + public void amount(Amount val) { put(Amount.Amount, val);} + public void balance(Amount val) { put(Amount.Balance, val);} + public void publicKey(Blob val) { put(Blob.PublicKey, val);} + public void signature(Blob val) { put(Blob.Signature, val);} + + public Hash256 channel() {return get(Hash256.Channel);} + public Amount amount() {return get(Amount.Amount);} + public Amount balance() {return get(Amount.Balance);} + public Blob publicKey() {return get(Blob.PublicKey);} + public Blob signature() {return get(Blob.Signature);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelCreate.java new file mode 100644 index 0000000000..2475c637e5 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelCreate.java @@ -0,0 +1,32 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class PaymentChannelCreate extends Transaction { + public PaymentChannelCreate() { + super(TransactionType.PaymentChannelCreate); + } + + public boolean hasDestinationTag() {return has(UInt32.DestinationTag);} + public boolean hasCancelAfter() {return has(UInt32.CancelAfter);} + + public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);} + public void cancelAfter(UInt32 val) { put(UInt32.CancelAfter, val);} + public void settleDelay(UInt32 val) { put(UInt32.SettleDelay, val);} + public void amount(Amount val) { put(Amount.Amount, val);} + public void publicKey(Blob val) { put(Blob.PublicKey, val);} + public void destination(AccountID val) { put(AccountID.Destination, val);} + + public UInt32 destinationTag() {return get(UInt32.DestinationTag);} + public UInt32 cancelAfter() {return get(UInt32.CancelAfter);} + public UInt32 settleDelay() {return get(UInt32.SettleDelay);} + public Amount amount() {return get(Amount.Amount);} + public Blob publicKey() {return get(Blob.PublicKey);} + public AccountID destination() {return get(AccountID.Destination);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelFund.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelFund.java new file mode 100644 index 0000000000..85ccff90f4 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelFund.java @@ -0,0 +1,24 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class PaymentChannelFund extends Transaction { + public PaymentChannelFund() { + super(TransactionType.PaymentChannelFund); + } + public boolean hasExpiration() {return has(UInt32.Expiration);} + + public UInt32 expiration() {return get(UInt32.Expiration);} + public Hash256 channel() {return get(Hash256.Channel);} + public Amount amount() {return get(Amount.Amount);} + + public void expiration(UInt32 val) { put(UInt32.Expiration, val);} + public void channel(Hash256 val) { put(Hash256.Channel, val);} + public void amount(Amount val) { put(Amount.Amount, val);} + +} + diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SetRegularKey.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SetRegularKey.java new file mode 100644 index 0000000000..5d139c75f9 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SetRegularKey.java @@ -0,0 +1,14 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class SetRegularKey extends Transaction { + public SetRegularKey() { + super(TransactionType.SetRegularKey); + } + public boolean hasRegularKey() {return has(AccountID.RegularKey);} + public AccountID regularKey() {return get(AccountID.RegularKey);} + public void regularKey(AccountID val) { put(AccountID.RegularKey, val);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SignerListSet.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SignerListSet.java new file mode 100644 index 0000000000..830381f8ff --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SignerListSet.java @@ -0,0 +1,20 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.STArray; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class SignerListSet extends Transaction { + public SignerListSet() { + super(TransactionType.SignerListSet); + } + public boolean hasSignerEntries() {return has(STArray.SignerEntries);} + + public UInt32 signerQuorum() {return get(UInt32.SignerQuorum);} + public STArray signerEntries() {return get(STArray.SignerEntries);} + + public void signerQuorum(UInt32 val) { put(UInt32.SignerQuorum, val);} + public void signerEntries(STArray val) { put(STArray.SignerEntries, val);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCancel.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCancel.java new file mode 100644 index 0000000000..a75f525e52 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCancel.java @@ -0,0 +1,17 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class TicketCancel extends Transaction { + public TicketCancel() { + super(TransactionType.TicketCancel); + } + public Hash256 ticketID() { + return get(Hash256.TicketID); + } + public void ticketID(Hash256 id) { + put(Hash256.TicketID, id); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCreate.java new file mode 100644 index 0000000000..4e8ac5fb3f --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCreate.java @@ -0,0 +1,22 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class TicketCreate extends Transaction { + public TicketCreate() { + super(TransactionType.TicketCreate); + } + + public boolean hasExpiration() {return has(UInt32.Expiration);} + public boolean hasTarget() {return has(AccountID.Target);} + + public UInt32 expiration() {return get(UInt32.Expiration);} + public AccountID target() {return get(AccountID.Target);} + + public void expiration(UInt32 val) { put(UInt32.Expiration, val);} + public void target(AccountID val) { put(AccountID.Target, val);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TrustSet.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TrustSet.java new file mode 100644 index 0000000000..ba5702c2e4 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TrustSet.java @@ -0,0 +1,25 @@ +package com.ripple.core.types.known.tx.txns; + +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class TrustSet extends Transaction { + public TrustSet() { + super(TransactionType.TrustSet); + } + + public UInt32 qualityIn() {return get(UInt32.QualityIn);} + public UInt32 qualityOut() {return get(UInt32.QualityOut);} + public Amount limitAmount() {return get(Amount.LimitAmount);} + public void qualityIn(UInt32 val) {put(Field.QualityIn, val);} + public void qualityOut(UInt32 val) {put(Field.QualityOut, val);} + public void limitAmount(Amount val) {put(Field.LimitAmount, val);} + + public boolean hasQualityIn() {return has(UInt32.QualityIn);} + public boolean hasQualityOut() {return has(UInt32.QualityOut);} + public boolean hasLimitAmount() {return has(Amount.LimitAmount);} + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/EnableAmendment.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/EnableAmendment.java new file mode 100644 index 0000000000..b65fc929e6 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/EnableAmendment.java @@ -0,0 +1,17 @@ +package com.ripple.core.types.known.tx.txns.pseudo; + +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class EnableAmendment extends Transaction { + public EnableAmendment() { + super(TransactionType.EnableAmendment); + } + + public UInt32 ledgerSequence() {return get(UInt32.LedgerSequence);} + public void ledgerSequence(UInt32 val) { put(UInt32.LedgerSequence, val);} + public Hash256 amendment() {return get(Hash256.Amendment);} + public void amendment(Hash256 val) { put(Hash256.Amendment, val);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/SetFee.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/SetFee.java new file mode 100644 index 0000000000..800f13e35f --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/SetFee.java @@ -0,0 +1,25 @@ +package com.ripple.core.types.known.tx.txns.pseudo; + +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.serialized.enums.TransactionType; +import com.ripple.core.types.known.tx.Transaction; + +public class SetFee extends Transaction { + public SetFee() { + super(TransactionType.SetFee); + } + public boolean hasLedgerSequence() {return has(UInt32.LedgerSequence);} + + public void ledgerSequence(UInt32 val) { put(UInt32.LedgerSequence, val);} + public void referenceFeeUnits(UInt32 val) { put(UInt32.ReferenceFeeUnits, val);} + public void reserveBase(UInt32 val) { put(UInt32.ReserveBase, val);} + public void reserveIncrement(UInt32 val) { put(UInt32.ReserveIncrement, val);} + public void baseFee(UInt64 val) { put(UInt64.BaseFee, val);} + + public UInt32 ledgerSequence() {return get(UInt32.LedgerSequence);} + public UInt32 referenceFeeUnits() {return get(UInt32.ReferenceFeeUnits);} + public UInt32 reserveBase() {return get(UInt32.ReserveBase);} + public UInt32 reserveIncrement() {return get(UInt32.ReserveIncrement);} + public UInt64 baseFee() {return get(UInt64.BaseFee);} +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/ledger/LedgerHeader.java b/ripple-core/src/main/java/com/ripple/core/types/ledger/LedgerHeader.java new file mode 100644 index 0000000000..240b0f6da8 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/ledger/LedgerHeader.java @@ -0,0 +1,109 @@ +package com.ripple.core.types.ledger; + +import com.ripple.core.binary.STReader; +import com.ripple.core.coretypes.RippleDate; +import com.ripple.core.coretypes.hash.HalfSha512; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.coretypes.uint.UInt64; +import com.ripple.core.coretypes.uint.UInt8; +import com.ripple.core.serialized.BinaryParser; +import com.ripple.core.serialized.BytesSink; +import org.json.JSONObject; +import org.json.JSONWriter; + +import java.util.Date; + +public class LedgerHeader { + // Always 0x4C475200 (LWR) (Secures signed objects) + public UInt32 version = HashPrefix.ledgerMaster.uInt32(); + + public UInt32 sequence; // Ledger Sequence (0 for genesis ledger) + public UInt64 totalXRP; // + public Hash256 previousLedger; // The hash of the previous ledger (0 for genesis ledger) + public Hash256 transactionHash; // The hash of the transaction tree's root node. + public Hash256 stateHash; // The hash of the state tree's root node. + public UInt32 parentCloseTime; // The time the previous ledger closed + public UInt32 closeTime; // UTC minute ledger closed encoded as seconds since 1/1/2000 (or 0 for genesis ledger) + public UInt8 closeResolution; // The resolution (in seconds) of the close time + public UInt8 closeFlags; // Flags + + public Date closeDate; + + public static LedgerHeader fromParser(BinaryParser parser) { + return fromReader(new STReader(parser)); + } + public static LedgerHeader fromHex(String ledger_data) { + return LedgerHeader.fromParser(new BinaryParser(ledger_data)); + } + + public static LedgerHeader fromReader(STReader reader) { + LedgerHeader ledger = new LedgerHeader(); + + ledger.sequence = reader.uInt32(); + ledger.totalXRP = reader.uInt64(); + ledger.previousLedger = reader.hash256(); + ledger.transactionHash= reader.hash256(); + ledger.stateHash = reader.hash256(); + ledger.parentCloseTime = reader.uInt32(); + ledger.closeTime = reader.uInt32(); + ledger.closeResolution = reader.uInt8(); + ledger.closeFlags = reader.uInt8(); + + ledger.closeDate = RippleDate.fromSecondsSinceRippleEpoch(ledger.closeTime); + + return ledger; + } + + public void toBytesSink(BytesSink sink) { + sequence.toBytesSink(sink); + totalXRP.toBytesSink(sink); + previousLedger.toBytesSink(sink); + transactionHash.toBytesSink(sink); + stateHash.toBytesSink(sink); + parentCloseTime.toBytesSink(sink); + closeTime.toBytesSink(sink); + closeResolution.toBytesSink(sink); + closeFlags.toBytesSink(sink); + } + + public Hash256 hash() { + HalfSha512 half = HalfSha512.prefixed256(HashPrefix.ledgerMaster); + toBytesSink(half); + return half.finish(); + } + + public void toJSONWriter(JSONWriter writer) { + writer.key("ledger_index"); + writer.value(sequence.toJSON()); + writer.key("total_coins"); + writer.value(totalXRP.toString(10)); + writer.key("parent_hash"); + writer.value(previousLedger.toJSON()); + writer.key("transaction_hash"); + writer.value(transactionHash.toJSON()); + writer.key("account_hash"); + writer.value(stateHash.toJSON()); + writer.key("close_time"); + writer.value(closeTime.toJSON()); + writer.key("close_time_human"); + // TODO + writer.value(RippleDate.gmtString(RippleDate.fromSecondsSinceRippleEpoch(closeTime))); + writer.key("parent_close_time"); + writer.value(parentCloseTime.toJSON()); + writer.key("close_time_resolution"); + writer.value(closeResolution.toJSON()); + writer.key("close_flags"); + writer.value(closeFlags.toJSON()); + } + + public JSONObject toJSON() { + StringBuilder builder = new StringBuilder(); + JSONWriter jsonWriter = new JSONWriter(builder); + jsonWriter.object(); + toJSONWriter(jsonWriter); + jsonWriter.endObject(); + return new JSONObject(builder.toString()); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountState.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountState.java new file mode 100644 index 0000000000..c6eaadd3fa --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountState.java @@ -0,0 +1,341 @@ +package com.ripple.core.types.shamap; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.Vector256; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.Index; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.enums.LedgerEntryType; +import com.ripple.core.types.known.sle.LedgerEntry; +import com.ripple.core.types.known.sle.LedgerHashes; +import com.ripple.core.types.known.sle.entries.DirectoryNode; +import com.ripple.core.types.known.sle.entries.OfferDirectory; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.json.JSONWriter; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.util.Iterator; + +public class AccountState extends ShaMap { + public AccountState() { + super(); + } + public AccountState(boolean isCopy, int depth) { + super(isCopy, depth); + } + + @Override + protected ShaMapInner makeInnerOfSameClass(int depth) { + return new AccountState(true, depth); + } + + private static LedgerHashes newSkipList(Hash256 skipIndex) { + LedgerHashes skip; + skip = new LedgerHashes(); + skip.put(UInt32.Flags, new UInt32(0)); + skip.hashes(new Vector256()); + skip.index(skipIndex); + return skip; + } + + public void updateSkipLists(long currentIndex, Hash256 parentHash) { + long prev = currentIndex - 1; + + if ((prev & 0xFF) == 0) { + Hash256 skipIndex = Index.ledgerHashes(prev); + LedgerHashes skip = createOrUpdateSkipList(skipIndex); + Vector256 hashes = skip.hashes(); + assert hashes.size() <= 256; + hashes.add(parentHash); + skip.put(UInt32.LastLedgerSequence, new UInt32(prev)); + } + + Hash256 skipIndex = Index.ledgerHashes(); + LedgerHashes skip = createOrUpdateSkipList(skipIndex); + Vector256 hashes = skip.hashes(); + + if (hashes.size() > 256) throw new AssertionError(); + if (hashes.size() == 256) { + hashes.remove(0); + } + + hashes.add(parentHash); + skip.put(UInt32.LastLedgerSequence, new UInt32(prev)); + } + + private LedgerHashes createOrUpdateSkipList(Hash256 skipIndex) { + PathToIndex path = pathToIndex(skipIndex); + ShaMapInner top = path.dirtyOrCopyInners(); + LedgerEntryItem item; + + if (path.hasMatchedLeaf()) { + ShaMapLeaf leaf = path.invalidatedPossiblyCopiedLeafForUpdating(); + item = (LedgerEntryItem) leaf.item; + } else { + item = new LedgerEntryItem(newSkipList(skipIndex)); + top.addLeafToTerminalInner(new ShaMapLeaf(skipIndex, item)); + } + return (LedgerHashes) item.entry; + } + + public boolean addLE(LedgerEntry entry) { + LedgerEntryItem item = new LedgerEntryItem(entry); + return addItem(entry.index(), item); + } + + public boolean updateLE(LedgerEntry entry) { + LedgerEntryItem item = new LedgerEntryItem(entry); + return updateItem(entry.index(), item); + } + + public LedgerEntry getLE(Hash256 index) { + LedgerEntryItem item = (LedgerEntryItem) getItem(index); + return item == null ? null : item.value(); + } + + public DirectoryNode getDirectoryNode(Hash256 index) { + return (DirectoryNode) getLE(index); + } + + public Iterable offerDirectories(Hash256 bookBase) { + final QualityIterator iter = qualityIterator(bookBase); + + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + boolean hasNext = iter.hasNext(); + // In case we need to skip some entries + if (hasNext && !(iter.next() instanceof OfferDirectory)) { + return this.hasNext(); + } + return hasNext; + } + + @Override + public OfferDirectory next() { + return (OfferDirectory) iter.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + public void writeEntriesArray(final JSONWriter writer) { + writer.array(); + walkEntries(entry -> writer.value(entry.toJSON())); + writer.endArray(); + } + + // Assumes shamap won't be modified during iteration, not unusual for an + // iterator. + public class QualityIterator implements Iterator { + ShaMapInner[] inners = new ShaMapInner[64]; + Hash256 base; + Hash256 end; + int[] selections = new int[64]; + int commonNibblets; + int depth; + ShaMapLeaf next; + private boolean finished = false; + + public QualityIterator(Hash256 start) { + base = start; + depth=0; + end = Index.bookEnd(start); + ShaMapInner inner = AccountState.this; + setInner(inner); + setSelected(start.nibblet(depth)); + findCommonNibblets(); + } + + private void findCommonNibblets() { + for (int i = 0; i < 64; i++) { + if (base.nibblet(i) == end.nibblet(i)) { + commonNibblets = i; + } else { + break; + } + } + } + + private void setInner(ShaMapInner inner) { + inners[depth] = inner; + } + + private void findNext() { + next = null; + + while (true) { + while (selected() > 15) { + depth--; + incrementSelection(); + } + if (depth < commonNibblets && selected() > end.nibblet(depth)) { + finished = true; + break; + } + ShaMapInner current = currentInner(); + ShaMapNode branch = current.getBranch(selected()); + if (branch == null) { + incrementSelection(); + } else if (branch.isInner()) { + depth++; + setInner(branch.asInner()); + setSelected(base.nibblet(depth)); + } else if (branch.isLeaf()) { + ShaMapLeaf leaf = branch.asLeaf(); + Hash256 leafIndex = leaf.index; + boolean leafIsOnPathToBase = depth < commonNibblets; + if ( leafIsOnPathToBase || leafIndex.compareTo(base) > 0 && + leafIndex.compareTo(end) < 0) { + next = leaf; + incrementSelection(); + } else { + finished = true; + } + break; + } + } + } + + private ShaMapInner currentInner() { + return inners[depth]; + } + private int selected() { + return selections[depth]; + } + private void setSelected(int nibblet) { + selections[depth] = nibblet; + } + private void incrementSelection() { + selections[depth]++; + } + + @Override + public boolean hasNext() { + findNext(); + return !finished && next != null; + } + @Override + public LedgerEntry next() { + // Just assume hasNext has been called + LedgerEntryItem item = (LedgerEntryItem) next.item; + return item.entry; + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + public QualityIterator qualityIterator(final Hash256 bookBase) { + return new QualityIterator(bookBase); + } + + public Iterable directoryIterator(OfferDirectory forQuality) { + // TODO: create an actual iterator + Vector256 indexes = new Vector256(); + OfferDirectory cursor = forQuality; + + while (cursor != null) { + indexes.addAll(cursor.indexes()); + if (cursor.hasNextIndex()) { + LedgerEntry le = getLE(cursor.nextIndex()); + if (le instanceof OfferDirectory) { + cursor = (OfferDirectory) le; + } + else { + break; + } + } + else { + break; + } + } + return indexes; + } + + public void walkEntries(final LedgerEntryVisitor walker) { + walkLeaves(leaf -> { + LedgerEntryItem item = (LedgerEntryItem) leaf.item; + walker.onEntry(item.entry); + }); + } + + // TODO + public Hash256 getNextIndex(Hash256 nextIndex, Hash256 bookEnd) { + return null; + } + + @Override + public AccountState copy() { + return (AccountState) super.copy(); + } + + public static AccountState loadFromLedgerDump(String filePath) { + try { + FileReader reader = new FileReader(filePath); + return loadFromLedgerDump(reader); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static AccountState loadFromLedgerDump(Reader reader) { + try { + JsonFactory jsonFactory = new JsonFactory(); + JsonParser parser = jsonFactory.createParser(reader); + JsonToken jsonToken = parser.nextToken(); + if (jsonToken != JsonToken.START_OBJECT) { + throw new AssertionError(); + } + + String accountState = "accountState"; + + // TODO: + while (parser.nextToken() != null) { + String currentName = parser.getCurrentName(); + if (currentName.equals(accountState)) { + break; + } + } + + if (!parser.getCurrentName().equals("accountState")) { + throw new IllegalStateException("No `accountState` field found!"); + } + + AccountState state = new AccountState(); + ObjectMapper mapper = new ObjectMapper(); + if (parser.nextToken() != JsonToken.START_ARRAY) { + throw new AssertionError(); + } + + while (parser.nextToken() != JsonToken.END_ARRAY) { + TreeNode treeNode = mapper.readTree(parser); + state.addLE((LedgerEntry) STObject.fromJacksonObject((ObjectNode) treeNode)); + } + return state; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountStateBuilder.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountStateBuilder.java new file mode 100644 index 0000000000..1b43203db7 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountStateBuilder.java @@ -0,0 +1,320 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.fields.Field; +import com.ripple.core.types.known.sle.LedgerEntry; +import com.ripple.core.types.known.sle.ThreadedLedgerEntry; +import com.ripple.core.types.known.sle.entries.*; +import com.ripple.core.types.known.tx.result.AffectedNode; +import com.ripple.core.types.known.tx.result.TransactionResult; + +import java.util.*; + +public class AccountStateBuilder { + private AccountState state; + private AccountState previousState = null; + private long targetLedgerIndex; + public long nextTransactionIndex = 0; + private Hash256 targetAccountHash; + public long totalTransactions = 0; + + private TreeSet directoriesModifiedMoreThanOnceByTransaction = new TreeSet<>(); + private TreeSet directoriesModifiedByTransaction = new TreeSet<>(); + public TreeSet ledgerModifiedEntries = new TreeSet<>(); + public TreeSet ledgerDeletedEntries = new TreeSet<>(); + + public boolean sortedDirectories = true; + + public void resetModified() { + ledgerModifiedEntries.clear(); + ledgerDeletedEntries.clear(); + directoriesModifiedMoreThanOnceByTransaction.clear(); + } + + public AccountStateBuilder(AccountState state, long targetLedgerIndex) { + this.state = state; + setStateCheckPoint(); + this.targetLedgerIndex = targetLedgerIndex; + } + + public void onLedgerClose(long ledgerIndex, Hash256 accountHash, Hash256 parentHash) { + state.updateSkipLists(ledgerIndex, parentHash); + targetLedgerIndex = ledgerIndex; + targetAccountHash = accountHash; + nextTransactionIndex = 0; + } + + public void setStateCheckPoint() { + previousState = state.copy(); + } + + public void onTransaction(TransactionResult tr) { + System.out.println("adding tx: " + tr.hash); + if (tr.meta.transactionIndex().longValue() != nextTransactionIndex) throw new AssertionError(); + if (tr.ledgerIndex.longValue() != targetLedgerIndex + 1) throw new AssertionError(String.format("%d != %d", tr.ledgerIndex.longValue(), targetLedgerIndex + 1)); + nextTransactionIndex++; + totalTransactions++; + directoriesModifiedByTransaction = new TreeSet<>(); + + for (AffectedNode an : sortedAffectedNodes(tr)) { + Hash256 id = an.ledgerIndex(); + LedgerEntry le = an.nodeAsFinal(); + + if (an.isCreatedNode()) { + ledgerModifiedEntries.add(id); + ledgerDeletedEntries.remove(id); + le.setDefaults(); + state.addLE(le); + + if (le instanceof Offer) { + Offer offer = (Offer) le; + offer.setOfferDefaults(); // TODO / TODO + + for (Hash256 directory : offer.directoryIndexes()) { + DirectoryNode dn = getDirectoryForUpdating(directory); + Hash256 index = offer.index(); + addToDirectoryNode(dn, index); + } + } else if (le instanceof RippleState) { + RippleState state = (RippleState) le; + + for (Hash256 directory : state.directoryIndexes()) { + DirectoryNode dn = getDirectoryForUpdating(directory); + addToDirectoryNode(dn, state.index()); + } + } + + if (le instanceof IndexedLedgerEntry) { + IndexedLedgerEntry owned = (IndexedLedgerEntry) le; + for (Hash256 directory : owned.ownerDirectoryIndexes(tr.txn)) { + DirectoryNode dn = getDirectoryForUpdating(directory); + addToDirectoryNode(dn, owned.index()); + } + } + + if (le instanceof ThreadedLedgerEntry) { + ThreadedLedgerEntry tle = (ThreadedLedgerEntry) le; + tle.previousTxnID(tr.hash); + tle.previousTxnLgrSeq(tr.ledgerIndex); + } + } else if (an.isDeletedNode()) { + ledgerModifiedEntries.remove(id); + ledgerDeletedEntries.add(id); + directoriesModifiedMoreThanOnceByTransaction.remove(id); + state.removeLeaf(id); + if (le instanceof Offer) { + Offer offer = (Offer) le; + for (Hash256 directory : offer.directoryIndexes()) { + DirectoryNode dn = getDirectoryForUpdating(directory); + if (dn != null) { +// Hash256 index = offer.index(); + if (dn.owner() != null) { + deleteFromDirectoryUnstable(offer, dn); + } else { + deleteFromDirectoryStable(offer, dn); + } + } + } + } else if (le instanceof RippleState) { + RippleState state = (RippleState) le; + for (Hash256 directory : state.directoryIndexes()) { + DirectoryNode dn = getDirectoryForUpdating(directory); + if (dn != null) { + deleteFromDirectoryUnstable(le, dn); + } + } + } else if (le instanceof IndexedLedgerEntry) { + IndexedLedgerEntry owned = (IndexedLedgerEntry) le; + for (Hash256 directory : owned.ownerDirectoryIndexes(tr.txn)) { + DirectoryNode dn = getDirectoryForUpdating(directory); + deleteFromDirectoryUnstable(le, dn); + } + } + } else if (an.isModifiedNode()) { + ledgerModifiedEntries.add(id); + ShaMapLeaf leaf = state.getLeafForUpdating(id); + LedgerEntryItem item = (LedgerEntryItem) leaf.item; + LedgerEntry leModded = item.entry; + + if (le instanceof ThreadedLedgerEntry) { + ThreadedLedgerEntry tle = (ThreadedLedgerEntry) le; + tle.previousTxnID(tr.hash); + tle.previousTxnLgrSeq(tr.ledgerIndex); + } + for (Field field : le) { + // Already have the `index` + if (field == Field.LedgerIndex) { + continue; + } + leModded.put(field, le.get(field)); + } + // Find all removed fields + ArrayList removed = new ArrayList<>(); + for (Field field : leModded) { + if (an.removedField(field)) { + removed.add(field); + } + } + removed.forEach(leModded::remove); + } + } + } + + private void deleteFromDirectoryUnstable(LedgerEntry b4, DirectoryNode dn) { + boolean b = directoryRemoveUnstable(dn, b4.index()); + DirectoryNode cursor = dn; + + if (!b) { + while (cursor.indexNext() != null) { + cursor = getDirectoryForUpdating(cursor.nextIndex()); + b = directoryRemoveUnstable(cursor, b4.index()); + if (b) { + break; + } + } + } +// + if (!b) { + cursor = dn; + while (cursor.indexPrevious() != null) { + cursor = getDirectoryForUpdating(cursor.prevIndex()); + b = directoryRemoveUnstable(cursor, b4.index()); + if (b) { + break; + } + } + } + } + private void deleteFromDirectoryStable(LedgerEntry b4, DirectoryNode dn) { + boolean b = directoryRemoveStable(dn, b4.index()); + DirectoryNode cursor = dn; + + if (!b) { + while (cursor.hasIndexPrevious()) { + cursor = getDirectoryForUpdating(cursor.prevIndex()); + b = directoryRemoveStable(cursor, b4.index()); + if (b) { + break; + } + } + } + if (!b) { + while (cursor.indexNext() != null) { + cursor = getDirectoryForUpdating(cursor.nextIndex()); + b = directoryRemoveStable(cursor, b4.index()); + if (b) { + break; + } + } + } + } + + public static Collection makeCollection(Iterable iter) { + Collection list = new ArrayList<>(); + for (E item : iter) { + list.add(item); + } + return list; + } + private ArrayList sortedAffectedNodes(TransactionResult tr) { + ArrayList sorted = new ArrayList<>(makeCollection(tr.meta.affectedNodes())); + sorted.sort(Comparator.comparingInt(this::getOrdinal)); + return sorted; + } + + private int getOrdinal(AffectedNode o1) { + switch (o1.ledgerEntryType()) { + case DirectoryNode: + return 10; + case RippleState: + return 20; + case Offer: + return 30; + case Escrow: + case SignerList: + case PayChannel: + return 21; + default: + return 40; + } + } + + private void onDirectoryModified(DirectoryNode dn) { + Hash256 index = dn.index(); + if (directoriesModifiedByTransaction.contains(index)) { + directoriesModifiedMoreThanOnceByTransaction.add(index); + } + else { + directoriesModifiedByTransaction.add(index); + } + } + private boolean directoryRemoveStable(DirectoryNode dn, Hash256 index) { + onDirectoryModified(dn); + return dn.indexes().remove(index); + } + private boolean directoryRemoveUnstable(DirectoryNode dn, Hash256 index) { + onDirectoryModified(dn); + if (sortedDirectories) { + return dn.indexes().remove(index); + } else { + return dn.indexes().removeUnstable(index); + } + } + private void addToDirectoryNode(DirectoryNode dn, Hash256 index) { + onDirectoryModified(dn); + if (dn.exchangeRate() != null) { + dn.indexes().add(index); + } else { + dn.indexes().add(index); + if (sortedDirectories) { + Collections.sort(dn.indexes()); + } + } + } + private DirectoryNode getDirectoryForUpdating(Hash256 directoryIndex) { + ShaMapLeaf leaf = state.getLeafForUpdating(directoryIndex); + if (leaf == null) { + return null; + } + LedgerEntryItem lei = (LedgerEntryItem) leaf.item; + return (DirectoryNode) lei.entry; + } + + public AccountState state() { + return state; + } + + public long currentLedgerIndex() { + return targetLedgerIndex; + } + + public String targetAccountHashHex() { + return targetAccountHash.toHex(); + } + public Hash256 targetAccountHash() { + return targetAccountHash; + } + + public TreeSet directoriesWithIndexesOutOfOrder() { + TreeSet ret = new TreeSet<>(); + for (Hash256 hash256 : directoriesModifiedMoreThanOnceByTransaction) { + DirectoryNode dn = state.getDirectoryNode(hash256); + if (dn.owner() != null) { + ret.add(hash256); + } + } + return ret; + } + + public boolean bad() { + return !state.hash().equals(targetAccountHash); + } + + public AccountState previousState() { + return previousState; + } + + public void setState(AccountState map) { + state = map; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/BytesItem.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/BytesItem.java new file mode 100644 index 0000000000..11365302b5 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/BytesItem.java @@ -0,0 +1,37 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.prefixes.Prefix; +import com.ripple.core.serialized.BytesSink; + +public class BytesItem extends ShaMapItem { + private byte[] item; + + public BytesItem(byte[] item) { + this.item = item; + } + + @Override + void toBytesSink(BytesSink sink) { + sink.add(item); + } + + @Override + public ShaMapItem copy() { + return this; + } + + @Override + public byte[] value() { + return item; + } + + @Override + public Prefix hashPrefix() { + return new Prefix() { + @Override + public byte[] bytes() { + return new byte[0]; + } + }; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/HashedTreeWalker.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/HashedTreeWalker.java new file mode 100644 index 0000000000..a565baf19b --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/HashedTreeWalker.java @@ -0,0 +1,8 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.Hash256; + +public interface HashedTreeWalker { + public void onLeaf(Hash256 h, ShaMapLeaf le); + public void onInner(Hash256 h, ShaMapInner inner); +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/LeafWalker.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/LeafWalker.java new file mode 100644 index 0000000000..71f371cebe --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/LeafWalker.java @@ -0,0 +1,5 @@ +package com.ripple.core.types.shamap; + +public interface LeafWalker { + void onLeaf(ShaMapLeaf shaMapLeaf); +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryItem.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryItem.java new file mode 100644 index 0000000000..ea67d4bf0b --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryItem.java @@ -0,0 +1,45 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.hash.prefixes.Prefix; +import com.ripple.core.fields.Field; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.types.known.sle.LedgerEntry; + +public class LedgerEntryItem extends ShaMapItem { + public LedgerEntryItem(LedgerEntry entry) { + this.entry = entry; + } + + public LedgerEntry entry; + + @Override + void toBytesSink(BytesSink sink) { + entry.toBytesSink(sink); + } + + @Override + public String toString() { + return entry.prettyJSON(); + } + + @Override + public ShaMapItem copy() { + STObject object = STObject.fromBytes(entry.toBytes()); + LedgerEntry le = (LedgerEntry) object; + // TODO: what about other auxiliary (non serialized) fields + le.index(entry.index()); + return new LedgerEntryItem(le); + } + + @Override + public LedgerEntry value() { + return entry; + } + + @Override + public Prefix hashPrefix() { + return HashPrefix.leafNode; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryVisitor.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryVisitor.java new file mode 100644 index 0000000000..6ce639b552 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryVisitor.java @@ -0,0 +1,7 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.types.known.sle.LedgerEntry; + +public interface LedgerEntryVisitor { + public void onEntry(LedgerEntry entry); +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/NodeStore.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/NodeStore.java new file mode 100644 index 0000000000..8d1bd98eee --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/NodeStore.java @@ -0,0 +1,64 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.HalfSha512; +import com.ripple.core.coretypes.hash.Hash256; + +/** + + * This is a toy implementation for illustrative purposes. + */ +public class NodeStore { + /** + * In ripple, all data is stored in a simple binary key/value database. + * The keys are 256 bit binary strings and the values are binary strings of + * arbitrary length. + */ + public static interface KeyValueBackend { + void put(Hash256 key, byte[] content); + byte[] get(Hash256 key); + } + + KeyValueBackend backend; + public NodeStore(KeyValueBackend backend) { + this.backend = backend; + } + /** + * All data stored is keyed by the hash of it's contents. + * Ripple uses the first 256 bits of a sha512 as it's 33 percent + * faster than using sha256. + * + * @return `key` used to store the content + */ + private Hash256 storeContent(byte[] content) { + HalfSha512 hasher = new HalfSha512(); + hasher.update(content); + Hash256 key = hasher.finish(); + storeHashKeyedContent(key, content); + return key; + } + + /** + * @param hash As ripple uses the `hash` of the contents as the + * NodeStore key, `hash` is pervasively used in lieu of + * the term `key`. + */ + private void storeHashKeyedContent(Hash256 hash, byte[] content) { + // Note: The real nodestore actually prepends some metadata, which doesn't + // contribute to the hash. + backend.put(hash, content); // metadata + content + } + + /** + * The complement to `set` api, which together form a simple public interface. + */ + public byte[] get(Hash256 hash) { + return backend.get(hash); + + } + /** + * The complement to `get` api, which together form a simple public interface. + */ + public Hash256 set(byte[] content) { + return storeContent(content); + } +} \ No newline at end of file diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/PathToIndex.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/PathToIndex.java new file mode 100644 index 0000000000..dcc0dfd56e --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/PathToIndex.java @@ -0,0 +1,156 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.Hash256; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Iterator; + +public class PathToIndex { + public Hash256 index; + public ShaMapLeaf leaf; + + private ArrayDeque inners; + private ShaMapInner[] dirtied; + private boolean matched = false; + + public boolean hasLeaf() { + return leaf != null; + } + public boolean leafMatchedIndex() { + return matched; + } + public boolean copyLeafOnUpdate() { + return leaf.version != dirtied[0].version; + } + + int size() { + return inners.size(); + } + + public ShaMapInner top() { + return dirtied[dirtied.length - 1]; + } + + // returns the + public ShaMapInner dirtyOrCopyInners() { + if (maybeCopyOnWrite()) { + int ix = 0; + // We want to make a uniformly accessed array of the inners + dirtied = new ShaMapInner[inners.size()]; + // from depth 0 to 1, to 2, to 3, don't be fooled by the api + Iterator it = inners.descendingIterator(); + + // This is actually the root which COULD be the top of the stack + // Think about it ;) + ShaMapInner top = it.next(); + dirtied[ix++] = top; + top.invalidate(); + + while (it.hasNext()) { + ShaMapInner next = it.next(); + boolean doCopies = next.version != top.version; + + if (doCopies) { + ShaMapInner copy = next.copy(top.version); + copy.invalidate(); + top.setBranch(index, copy); + next = copy; + } else { + next.invalidate(); + } + top = next; + dirtied[ix++] = top; + } + return top; + } else { + copyInnersToDirtiedArray(); + return inners.peekFirst(); + } + } + + public boolean hasMatchedLeaf() { + return hasLeaf() && leafMatchedIndex(); + } + + public void collapseOnlyLeafChildInners() { + assert dirtied != null; + + ShaMapInner next; + ShaMapLeaf onlyChild = null; + + for (int i = dirtied.length - 1; i >= 0; i--) { + next = dirtied[i]; + if (onlyChild != null) { + next.setLeaf(onlyChild); + } + onlyChild = next.onlyChildLeaf(); + if (onlyChild == null) { + break; + } + } + } + + private void copyInnersToDirtiedArray() { + int ix = 0; + dirtied = new ShaMapInner[inners.size()]; + Iterator descending = inners.descendingIterator(); + while (descending.hasNext()) { + ShaMapInner next = descending.next(); + dirtied[ix++] = next; + next.invalidate(); + } + } + + private boolean maybeCopyOnWrite() { + return inners.peekLast().doCoW; + } + + public PathToIndex(ShaMapInner root, Hash256 index) { + this.index = index; + makeStack(root, index); + } + + private void makeStack(ShaMapInner root, Hash256 index) { + inners = new ArrayDeque<>(); + ShaMapInner top = root; + + while (true) { + inners.push(top); + ShaMapNode existing = top.getBranch(index); + if (existing == null) { + break; + } else if (existing.isLeaf()) { + leaf = existing.asLeaf(); + matched = leaf.index.equals(index); + break; + } + else if (existing.isInner()) { + top = existing.asInner(); + } + } + } + + public ShaMapLeaf invalidatedPossiblyCopiedLeafForUpdating() { + assert matched; + if (dirtied == null) { + dirtyOrCopyInners(); + } + ShaMapLeaf theLeaf = leaf; + + if (copyLeafOnUpdate()) { + theLeaf = leaf.copy(); + top().setLeaf(theLeaf); + } + theLeaf.invalidate(); + return theLeaf; + } + + public ArrayList topDownList() { + ArrayList path = new ArrayList<>(); + Iterator shaMapInnerIterator = inners.descendingIterator(); + shaMapInnerIterator.forEachRemaining(path::add); + path.add(leaf); + return path; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/README.md b/ripple-core/src/main/java/com/ripple/core/types/shamap/README.md new file mode 100644 index 0000000000..a78bc3ad73 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/README.md @@ -0,0 +1,319 @@ +Rippled NodeStore +----------------- + +To understand a ShaMap first you must know about the NodeStore. + +```java +/** + + * This is a toy implementation for illustrative purposes. + */ +public class NodeStore { + /** + * In ripple, all data is stored in a simple binary key/value database. + * The keys are 256 bit binary strings and the values are binary strings of + * arbitrary length. + */ + public interface KeyValueBackend { + void put(Hash256 key, byte[] content); + byte[] get(Hash256 key); + } + + KeyValueBackend backend; + public NodeStore(KeyValueBackend backend) { + this.backend = backend; + } + /** + * All data stored is keyed by the hash of it's contents. + * Ripple uses the first 256 bits of a sha512 as it's 33 percent + * faster than using sha256. + * + * @return `key` used to store the content + */ + private Hash256 storeContent(byte[] content) { + Hash256.HalfSha512 hasher = new Hash256.HalfSha512(); + hasher.update(content); + Hash256 key = hasher.finish(); + storeHashKeyedContent(key, content); + return key; + } + + /** + * @param hash As ripple uses the `hash` of the contents as the + * NodeStore key, `hash` is pervasively used in lieu of + * the term `key`. + */ + private void storeHashKeyedContent(Hash256 hash, byte[] content) { + // Note: The real NodeStore actually prepends some metadata, which doesn't + // contribute to the hash. + backend.put(hash, content); // metadata + content + } + + /** + * The complement to `set` api, which together form a simple public interface. + */ + public byte[] get(Hash256 hash) { + return backend.get(hash); + + } + /** + * The complement to `get` api, which together form a simple public interface. + */ + public Hash256 set(byte[] content) { + return storeContent(content); + } +} +``` + +See also: +* [Serialized Types](../../../../../../../../README.md) +* [BinaryFormats.txt (historical)](https://github.com/ripple/rippled/blob/07df5f1f81b0ee1ab641d134ba8e940a90f5297e/BinaryFormats.txt#L2-L6) + +Excerpt from BinaryFormats.txt (historical): + +
+ All signed or hashed objects must have well-defined binary formats at the + byte level. These formats do not have to be the same as the network wire + formats or the forms used for efficient storage or human display. However, + it must always be possible to precisely re-create, byte for byte, any signed + or hashed object. Otherwise, the signatures or hashes cannot be validated. +
+ +Note that currently (26/Jan/2018) the NodeStore stores it in the hashing form. + +What is a ShaMap? +----------------- + +A ShaMap is a special type of tree, used as a way to index values stored in a +`NodeStore` + +Recall that values in the NodeStore are keyed by the hash of their contents. + +But what about identities that change over time? How can you retrieve a certain +version of something? What could be used as an enduring identifier? The value +must have some component[s] that are static over time. These are fed into a +hashing function to create a 256 bit identifier. + +But how is this used? You can only query values by `hash` in the NodeStore. The +hash, as a function of a value would obviously change along with it. + +The identifier is used as an `index` into a ShaMap tree, which in ripple, is +representative of a point in time. In fact a ShaMap can be hashed +deterministically, thus a point in time can be identified by a `hash`. Where is +a ShaMap actually stored? In the NodeStore, of course. + +But the NodeStore only stores binary content you protest! But the ShaMap has a +binary representation! So what are the `contents` of a ShaMap, to be hashed to +be stored in the NodeStore? + +Glad you asked. A tree, has a root, and many children, either more branches, or +terminal leaves. The root, and any of its children that have children +themselves, are classed as `inner nodes`. + +In a ShaMap these `inner nodes` each have 16 `slots` for children. The binary +representation of these is simply 16 `hash`es, used to retrieve child nodes from +the NodeStore. + +An example of an inner node's `contents` + + Empty slots are represented as 32 0 bytes + + ``` + 022CC592F5D4ABC3A63DA2A036CDDC0825B30717C78EF287BEF200056133FDA2 + 0000000000000000000000000000000000000000000000000000000000000000 + BEE626551799DDFE65BD2D9A0F0EA24D72C93CFD8E083176718D2B079EC60214 + E1B34F1D9209CB668A50CCEE71C8109D140A6D715D923AEE98E6D53015D8B66B + 4C27A856094CFDE37CD2A0EA93DADB595B10CFEC55F816C987A6AC48D13AF5C0 + 2F770714A9EF92792F44AA1537C18F68AFE3FFF157FB9088FFE2BDA695C19B71 + C915CA982310CF41CF1266AA43C3B31ACBF4304D05ADB54A352D942C890763A3 + F29FAD442CE204513BEA555A4192E324407444D946449CEA510C37A9BB982134 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 44B3BE10744EA2DA010D530C6AE64E3C3984DA7701EE79A66EA429EC11B87D1D + 0C7BE8569E9F08BADDEB91EBE79E5B98BBF245B067B7B83B4A95430CFEC9F7E8 + BB38EA169DB6A020EA820BD1242DDB6250B397A26015507BA3C7F3C041EA683C + 0000000000000000000000000000000000000000000000000000000000000000 + 15B98934D22B5CB7233C42CE8DC8DD0D2328AB91CC574332C45D7160BD31D4AD + 1894E389AE4A63BA99C2D0546A58A976ECDAB14C09B98F532999B464696E29E5 + ``` + +What about those `index` thingies again? Remember, the `index` can't be used to +query something directly in the NodeStore. + +First you need a known point in time, which we learned could be defined by a +`hash` of a ShaMap. + +* shamap hash: 2F049AEE51C7C96AB911AF86E3278F4F90A38D196422A832617FF0C6F29C3704 +* value index: FEEE5CC92B64375C8FEE56D54A82B9965E44FE0DCF673DBF27D0AA93F8AFF4FB + +Imagine we have those above. First we query the NodeStore with `2F049A...` + +What do we expect back? A ShaMap hash is the hash of the binary representation +of the root node (which is an `inner node`) of the tree, so we'd expect +something in the form shown earlier, with 16 256 bit hashes. + +From the NodeStore we retrieve: + + ``` + 4D494E00 + 3C926652404076CA35EA1D89580975C50DCD0B29EC16079F168BE63BB1D6F237 + F0D3F178C56C438C597D53FF5E3D44EE3865C17A0D25DCC2460F20BE4BEB4B7E + D65D6CA291E451E6D6566A5431D755A749D9B0450E83B376148A6F93349ACD46 + 309B18093008055BE23E118DA4243DFC9AEF66E3260E2008BD6E6E6806F06725 + 0000000000000000000000000000000000000000000000000000000000000000 + D7D5A7C89DAE6D7517EDCB0601829CB339376EECD1DE8CA0216B6C18FFD77C32 + 7C33AEB525B09A7BC013C01015D59A88129A8A6C18B45E6D7D0CA1F13E6FCB44 + AB35119A7529D4BA57C41D7020833A0E9600895F61E9E9DEABBFDF82A877CBB2 + 3BE7A48570263034E6DCBEFA6ED57B63C0592AE145341A79DAF57C3BDF7C5793 + 17006AD967A1536E0441517990AD5B42890A3168AC9E045EAF9AC5A06F672561 + 94B12B8BB76D525DD9320AF51893A6D69FF730C1E647181E7A7B51FE1FBF369E + 0579A72B3209AE66840F6E8317F422F43937E6AE1A14C8B30243DE016551643F + B873AA4EF1CDF0332C59BE423256610C4C6A1D32E2A1F8DE748390E29E8FC2D3 + EB03A93BBC95ED56FB73E6FBDAB8F6916BBD1BD358336CBA4DA673558F5B068C + E64A78A07CF33C3B74AEA9862B5758513677324D4C9D72C69D14B4EB64D4EB02 + 4E8C0CE75693B85A4CAC5516B77702E660A55281CF17DBE09552941F3078D81C + ``` + +We see the hashes for 16 nodes clear as day, but what is this `4D494E00` +prefixed to the front? Converted to `ascii` letters the hex `4D494E00` is +`MIN\x00`, meaning sham)ap i)nner n)ode. + +The prefix `namespaces` the content, so different classes of objects which +would otherwise have the same binary representation, will have a different +`hash`. These `hash prefixes` serve another useful purpose, as we'll see later. +(Similarly, there are namespacing prefixes for an `index` (created by + feeding static components of an identity into a hashing function)) + +Is our value `index` hash amongst those enumerated? No !!! So what do we do with +it? An index, usually means an ordinal, defining a place in an array. The +`index` is actually an index into 64 arrays. Each nibble in the `index` is an +index into the 16 slots in each inner node. + +Consider again the value `index`: + + `FEEE5CC92B64375C8FEE56D54A82B9965E44FE0DCF673DBF27D0AA93F8AFF4FB` + +To use the `index` we take the first nibble, `F` (yes, we go left to right) + +The letter `F` in hex has the ordinal value 15, so we take the 16th branch (0 +based indexing) + +We select the 16th hash + + `4E8C0CE75693B85A4CAC5516B77702E660A55281CF17DBE09552941F3078D81C` + +From the NodeStore we retrieve: + + ``` + 4D494E00 + 0000000000000000000000000000000000000000000000000000000000000000 + 286BC64A4A369857E4B0B5834C54CED797110D06E64F7759FBA2C87D3630D418 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 60631B91674954D21314B89CEE3B7F740B4FB1374610498EA1A7A3B79BA6706D + 3B1C36A6311256FE1E72BEDBA2042D67D86F561E51B387CFA680BCE9982CD6DB + 5568E7032EE018CA484C181CD68411B623229D0666AE0038708500C59402A282 + 0000000000000000000000000000000000000000000000000000000000000000 + ``` + +There's that 'MIN\x00` hash prefix again. + +In fact, this prefix is how we can deterministically say that this is an +`inner node` and that we can interpret the following bytes as 16 more +`hash`es. + +We have descended deeper into the tree, but it seems we need to go deeper. We +are currently at a depth of 2, so to go deeper we need the 2nd nibble. + +value index: + ``` + FEEE5CC92B64375C8FEE56D54A82B9965E44FE0DCF673DBF27D0AA93F8AFF4FB` + | + \ + 2nd nibble + ``` + +The letter `E` in hex has the ordinal value 14, so we take the 15th branch (0 +based indexing) + +We select the 15th hash: + + `5568E7032EE018CA484C181CD68411B623229D0666AE0038708500C59402A282` + +From the NodeStore we retrieve: + + ``` + 534E4400 + C11C12000722800000002400AA7BFF201900AA7BFC201B0226FF9464D4461B5C + A191A906000000000000000000000000455448000000000006CC4A6D023E68AA + 3499C6DE3E9F2DC52B8BA25465400000000861014A6840000000000000C17321 + 03CF1DFB34A96363FF2B91638FCE51E6D7B88419729E9A81ABC99A9512FFB9C7 + 3374463044022005ED4635AE246A4060378D9396CAA0E42F7E9129D309B2E98B + DF5CE2352520A002207D4494019EA4327F4087EA34B05CE9E5CD0AE3082BAD77 + CC3698CE5A59C21BD581140BEC53D0830ADCE9E372086E570809916C440E83C3 + F4201C00000033F8E311006F561BF352EEDB9072286286A4BBC8C419C995A171 + 0AA0CFA8B0D2B843F07D294F60E82400AA7BFF501090B86A84C7F7843673BCF8 + 2E565E69498CAEF463F8055ABA4C04581E76C9270064D4461B5CA191A9060000 + 00000000000000000000455448000000000006CC4A6D023E68AA3499C6DE3E9F + 2DC52B8BA25465400000000861014A81140BEC53D0830ADCE9E372086E570809 + 916C440E83E1E1E31100645690B86A84C7F7843673BCF82E565E69498CAEF463 + F8055ABA4C04581E76C92700E8364C04581E76C927005890B86A84C7F7843673 + BCF82E565E69498CAEF463F8055ABA4C04581E76C92700011100000000000000 + 00000000004554480000000000021106CC4A6D023E68AA3499C6DE3E9F2DC52B + 8BA254E1E1E41100645690B86A84C7F7843673BCF82E565E69498CAEF463F805 + 5ABA4C045835BF30CCBFE72200000000364C045835BF30CCBF5890B86A84C7F7 + 843673BCF82E565E69498CAEF463F8055ABA4C045835BF30CCBF011100000000 + 00000000000000004554480000000000021106CC4A6D023E68AA3499C6DE3E9F + 2DC52B8BA2540311000000000000000000000000000000000000000004110000 + 000000000000000000000000000000000000E1E1E411006F56AE4B62A73DC540 + 40E523FCE863981AB601B6D6BCD3CFCE303B43A430FC6D4DB1E7220000000024 + 00AA7BFC250226FF9133000000000000000034000000000000000055B61A418E + 421021CFBA22B02F4934B4F79B1091F91CF910AEB21DA0975CEC52AB501090B8 + 6A84C7F7843673BCF82E565E69498CAEF463F8055ABA4C045835BF30CCBF64D4 + 461B7D5C693EDF000000000000000000000000455448000000000006CC4A6D02 + 3E68AA3499C6DE3E9F2DC52B8BA25465400000000861014A81140BEC53D0830A + DCE9E372086E570809916C440E83E1E1E511006456B937CC88FCAF18886CC8D4 + B19A5326F56B0B84E06AE511407B072DE348E05376E722000000003100000000 + 0000000032000000000000000058B937CC88FCAF18886CC8D4B19A5326F56B0B + 84E06AE511407B072DE348E0537682140BEC53D0830ADCE9E372086E57080991 + 6C440E83E1E1E5110061250226FF915558E2CDAB1D8D5477D8FED6EA5EFD9F9F + D8FAC56EA907E94197900C6C6175CE9556E9293AF964F2B20467530673C8F327 + 41ED03DEF52F6B6625B4D837C155856C26E62400AA7BFF6240000000B60EBC57 + E1E722000000002400AA7C002D000000186240000000B60EBB9681140BEC53D0 + 830ADCE9E372086E570809916C440E83E1E1F1031000 + FEEE5CC92B64375C8FEE56D54A82B9965E44FE0DCF673DBF27D0AA93F8AFF4FB + ``` + +Well, here's something new. The `hash prefix` is different. This time the +hex decodes as `MLN\x00`, meaning sham)ap l)eaf n)ode. + +And what's that at the end? Is that our index? It is!! + +Why does it need to be stored? We have only used `FE` to traverse to this +node. Without storing the `index` identifier in the leaf node contents, +there would be no way to be certain that this leaf held the item you wanted. +More importantly, it acts as further name-spacing, to prevent collisions. +(Technically, you could synthesize the index, by parsing the contents of +the object and recreating it) + +Takeaways +--------- + +* A `hash` keys the NodeStore +* An `index` is a path to an item in a ShaMap +* For communication purposes + - Always use `hash` when referring to a key for the NodeStore + - Always use `index` when referring to a key for a ShaMap + +Links +----- + +* [Rippled Hash Prefix declarations](../../coretypes/hash/prefixes/HashPrefix.java) \ No newline at end of file diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMap.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMap.java new file mode 100644 index 0000000000..0129eb7c4a --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMap.java @@ -0,0 +1,30 @@ +package com.ripple.core.types.shamap; + +import java.util.concurrent.atomic.AtomicInteger; + +public class ShaMap extends ShaMapInner { + private AtomicInteger copies; + + public ShaMap() { + super(0); + // This way we can copy the first to the second, + // copy the second, then copy the first again ;) + copies = new AtomicInteger(); + } + public ShaMap(boolean isCopy, int depth) { + super(isCopy, depth, 0); + } + + @Override + protected ShaMapInner makeInnerOfSameClass(int depth) { + return new ShaMap(true, depth); + } + + public ShaMap copy() { + version = copies.incrementAndGet(); + ShaMap copy = (ShaMap) copy(copies.incrementAndGet()); + copy.copies = copies; + return copy; + } + +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapDiff.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapDiff.java new file mode 100644 index 0000000000..41e23d50e1 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapDiff.java @@ -0,0 +1,122 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.Hash256; + +import java.util.TreeSet; + +public class ShaMapDiff { + public ShaMap one, two; + + public TreeSet modified = new TreeSet<>(); + public TreeSet deleted = new TreeSet<>(); + public TreeSet added = new TreeSet<>(); + + public ShaMapDiff(ShaMap one, ShaMap two) { + this.one = one; + this.two = two; + } + + // Find what's added, modified and deleted in `two` + public ShaMapDiff find() { + one.hash(); + two.hash(); + compare(one, two); + return this; + } + + public ShaMapDiff inverted() { + ShaMapDiff shaMapDiff = new ShaMapDiff(two, one); + + shaMapDiff.added = deleted; + shaMapDiff.modified = modified; + shaMapDiff.deleted = added; + + return shaMapDiff; + } + + public void apply(ShaMap sa) { + for (Hash256 mod : modified) { + boolean modded = sa.updateItem(mod, two.getItem(mod).copy()); + if (!modded) throw new AssertionError(); + } + + for (Hash256 add : added) { + boolean added = sa.addItem(add, two.getItem(add).copy()); + if (!added) throw new AssertionError(); + } + for (Hash256 delete : deleted) { + boolean removed = sa.removeLeaf(delete); + if (!removed) throw new AssertionError(); + } + } + private void compare(ShaMapInner a, ShaMapInner b) { + for (int i = 0; i < 16; i++) { + ShaMapNode aChild = a.getBranch(i); + ShaMapNode bChild = b.getBranch(i); + + if (aChild == null && bChild != null) { + trackAdded(bChild); + // added in B + } else if (aChild != null && bChild == null) { + trackRemoved(aChild); + // removed from B + } else if (aChild != null && !aChild.hash().equals(bChild.hash())) { + boolean aleaf = aChild.isLeaf(), + bLeaf = bChild.isLeaf(); + + if (aleaf && bLeaf) { + ShaMapLeaf la = (ShaMapLeaf) aChild; + ShaMapLeaf lb = (ShaMapLeaf) bChild; + if (la.index.equals(lb.index)) { + modified.add(la.index); + } else { + deleted.add(la.index); + added.add(lb.index); + } + } else if (aleaf /*&& bInner*/) { + ShaMapLeaf la = (ShaMapLeaf) aChild; + ShaMapInner ib = (ShaMapInner) bChild; + trackAdded(ib); + + //noinspection Duplicates + if (ib.hasLeaf(la.index)) { + // because trackAdded would have added it + added.remove(la.index); + ShaMapLeaf leaf = ib.getLeaf(la.index); + if (!leaf.hash().equals(la.hash())) { + modified.add(la.index); + } + } else { + deleted.add(la.index); + } + } else if (bLeaf /*&& aInner*/) { + ShaMapLeaf lb = (ShaMapLeaf) bChild; + ShaMapInner ia = (ShaMapInner) aChild; + trackRemoved(ia); + + //noinspection Duplicates + if (ia.hasLeaf(lb.index)) { + // because trackRemoved would have deleted it + deleted.remove(lb.index); + ShaMapLeaf leaf = ia.getLeaf(lb.index); + if (!leaf.hash().equals(lb.hash())) { + modified.add(lb.index); + } + } else { + added.add(lb.index); + } + } else /*if (aInner && bInner)*/ { + compare((ShaMapInner) aChild, (ShaMapInner) bChild); + } + } + } + } + + private void trackRemoved(ShaMapNode child) { + child.walkAnyLeaves(leaf -> deleted.add(leaf.index)); + } + + private void trackAdded(ShaMapNode child) { + child.walkAnyLeaves(leaf -> added.add(leaf.index)); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapInner.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapInner.java new file mode 100644 index 0000000000..f7d73dae65 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapInner.java @@ -0,0 +1,331 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.hash.prefixes.Prefix; +import com.ripple.core.serialized.BytesSink; + +import java.util.Iterator; + +public class ShaMapInner extends ShaMapNode implements Iterable { + public int depth; + int slotBits = 0; + int version = 0; + boolean doCoW; + protected ShaMapNode[] branches = new ShaMapNode[16]; + + public ShaMapInner(int depth) { + this(false, depth, 0); + } + + public ShaMapInner(boolean isCopy, int depth, int version) { + this.doCoW = isCopy; + this.depth = depth; + this.version = version; + } + + protected ShaMapInner copy(int version) { + ShaMapInner copy = makeInnerOfSameClass(depth); + System.arraycopy(branches, 0, copy.branches, 0, branches.length); + copy.slotBits = slotBits; + copy.hash = hash; + copy.version = version; + doCoW = true; + + return copy; + } + + protected ShaMapInner makeInnerOfSameClass(int depth) { + return new ShaMapInner(true, depth, version); + } + + protected ShaMapInner makeInnerChild() { + int childDepth = depth + 1; + if (childDepth >= 64) throw new AssertionError(); + return new ShaMapInner(doCoW, childDepth, version); + } + + // Descend into the tree, find the leaf matching this index + // and if the tree has it. + protected void setLeaf(ShaMapLeaf leaf) { + if (leaf.version == -1) { + leaf.version = version; + } + setBranch(leaf.index, leaf); + } + + private void removeBranch(Hash256 index) { + removeBranch(selectBranch(index)); + } + + public void walkLeaves(LeafWalker leafWalker) { + for (ShaMapNode branch : branches) { + if (branch != null) { + if (branch.isInner()) { + branch.asInner().walkLeaves(leafWalker); + } else if (branch.isLeaf()) { + leafWalker.onLeaf(branch.asLeaf()); + } + } + } + } + + public void walkTree(TreeWalker treeWalker) { + treeWalker.onInner(this); + for (ShaMapNode branch : branches) { + if (branch != null) { + if (branch.isLeaf()) { + ShaMapLeaf ln = branch.asLeaf(); + treeWalker.onLeaf(ln); + } else if (branch.isInner()) { + ShaMapInner childInner = branch.asInner(); + childInner.walkTree(treeWalker); + } + } + } + + } + + public void walkHashedTree(HashedTreeWalker walker) { + walker.onInner(hash(), this); + + for (ShaMapNode branch : branches) { + if (branch != null) { + if (branch.isLeaf()) { + ShaMapLeaf ln = branch.asLeaf(); + walker.onLeaf(branch.hash(), ln); + } else if (branch.isInner()) { + ShaMapInner childInner = branch.asInner(); + childInner.walkHashedTree(walker); + } + } + } + } + + /** + * @return the `only child` leaf or null if other children + */ + public ShaMapLeaf onlyChildLeaf() { + ShaMapLeaf leaf = null; + int leaves = 0; + + for (ShaMapNode branch : branches) { + if (branch != null) { + if (branch.isInner()) { + leaf = null; + break; + } else if (++leaves == 1) { + leaf = branch.asLeaf(); + } else { + leaf = null; + break; + } + } + } + return leaf; + } + + public boolean removeLeaf(Hash256 index) { + PathToIndex path = pathToIndex(index); + if (path.hasMatchedLeaf()) { + ShaMapInner top = path.dirtyOrCopyInners(); + top.removeBranch(index); + path.collapseOnlyLeafChildInners(); + return true; + } else { + return false; + } + } + + public ShaMapItem getItem(Hash256 index) { + ShaMapLeaf leaf = getLeaf(index); + + @SuppressWarnings("unchecked") + ShaMapItem shaMapItem = leaf == null ? null : leaf.item; + return shaMapItem; + } + + public boolean addItem(Hash256 index, ShaMapItem item) { + return addLeaf(new ShaMapLeaf(index, item)); + } + + public boolean updateItem(Hash256 index, ShaMapItem item) { + return updateLeaf(new ShaMapLeaf(index, item)); + } + + public boolean hasLeaf(Hash256 index) { + return pathToIndex(index).hasMatchedLeaf(); + } + + public ShaMapLeaf getLeaf(Hash256 index) { + PathToIndex stack = pathToIndex(index); + if (stack.hasMatchedLeaf()) { + return stack.leaf; + } else { + return null; + } + } + + public boolean addLeaf(ShaMapLeaf leaf) { + PathToIndex stack = pathToIndex(leaf.index); + if (stack.hasMatchedLeaf()) { + return false; + } else { + ShaMapInner top = stack.dirtyOrCopyInners(); + top.addLeafToTerminalInner(leaf); + return true; + } + } + + public boolean updateLeaf(ShaMapLeaf leaf) { + PathToIndex stack = pathToIndex(leaf.index); + if (stack.hasMatchedLeaf()) { + ShaMapInner top = stack.dirtyOrCopyInners(); + // Why not update in place? Because of structural sharing + top.setLeaf(leaf); + return true; + } else { + return false; + } + } + + public PathToIndex pathToIndex(Hash256 index) { + return new PathToIndex(this, index); + } + + /** + * This should only be called on the deepest inners, as it + * does not do any dirtying. + * @param leaf to add to inner + */ + void addLeafToTerminalInner(ShaMapLeaf leaf) { + ShaMapNode branch = getBranch(leaf.index); + if (branch == null) { + setLeaf(leaf); + } else if (branch.isInner()) { + // This should never be called + throw new AssertionError(); + } else if (branch.isLeaf()) { + ShaMapInner inner = makeInnerChild(); + setBranch(leaf.index, inner); + inner.addLeafToTerminalInner(leaf); + inner.addLeafToTerminalInner(branch.asLeaf()); + } + } + + protected void setBranch(Hash256 index, ShaMapNode node) { + setBranch(selectBranch(index), node); + } + + protected ShaMapNode getBranch(Hash256 index) { + return getBranch(index.nibblet(depth)); + } + + public ShaMapNode getBranch(int i) { + return branches[i]; + } + + public ShaMapNode branch(int i) { + return branches[i]; + } + + protected int selectBranch(Hash256 index) { + return index.nibblet(depth); + } + + public boolean hasLeaf(int i) { + return branches[i].isLeaf(); + } + public boolean hasInner(int i) { + return branches[i].isInner(); + } + public boolean hasNone(int i) {return branches[i] == null;} + + private void setBranch(int slot, ShaMapNode node) { + slotBits = slotBits | (1 << slot); + branches[slot] = node; + invalidate(); + } + + private void removeBranch(int slot) { + branches[slot] = null; + slotBits = slotBits & ~(1 << slot); + } + public boolean empty() { + return slotBits == 0; + } + + @Override public boolean isLeaf() { return false; } + @Override public boolean isInner() { return true; } + + @Override + Prefix hashPrefix() { + return HashPrefix.innerNode; + } + + @Override + public void toBytesSink(BytesSink sink) { + for (ShaMapNode branch : branches) { + if (branch != null) { + branch.hash().toBytesSink(sink); + } else { + Hash256.ZERO_256.toBytesSink(sink); + } + } + } + + @Override + public Hash256 hash() { + if (empty()) { + // empty inners have a hash of all ZERO + // it's only valid for a root node to be empty + // any other inner node, must contain at least a + // single leaf + assert depth == 0; + return Hash256.ZERO_256; + } else { + // hash the hashPrefix() and toBytesSink + return super.hash(); + } + } + + public ShaMapLeaf getLeafForUpdating(Hash256 leaf) { + PathToIndex path = pathToIndex(leaf); + if (path.hasMatchedLeaf()) { + return path.invalidatedPossiblyCopiedLeafForUpdating(); + } + return null; + } + + @Override + public Iterator iterator() { + return new Iterator() { + int ix = 0; + + @Override + public boolean hasNext() { + return ix != 16; + } + + @Override + public ShaMapNode next() { + return branch(ix++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + public int branchCount() { + int populated = 0; + for (ShaMapNode branch : branches) { + if (branch != null) { + populated ++; + } + } + return populated; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapItem.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapItem.java new file mode 100644 index 0000000000..a8ec303dbb --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapItem.java @@ -0,0 +1,11 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.prefixes.Prefix; +import com.ripple.core.serialized.BytesSink; + +abstract public class ShaMapItem { + abstract void toBytesSink(BytesSink sink); + public abstract ShaMapItem copy(); + public abstract T value(); + public abstract Prefix hashPrefix(); +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapLeaf.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapLeaf.java new file mode 100644 index 0000000000..3a79dacb94 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapLeaf.java @@ -0,0 +1,34 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.prefixes.Prefix; +import com.ripple.core.serialized.BytesSink; + +public class ShaMapLeaf extends ShaMapNode { + public Hash256 index; + public ShaMapItem item; + public long version = -1; + + protected ShaMapLeaf(Hash256 index, ShaMapItem item) { + this.index = index; + this.item = item; + } + + @Override public boolean isLeaf() {return true;} + @Override public boolean isInner() {return false;} + + @Override + Prefix hashPrefix() { + return item.hashPrefix(); + } + + @Override + public void toBytesSink(BytesSink sink) { + item.toBytesSink(sink); + index.toBytesSink(sink); + } + + public ShaMapLeaf copy() { + return new ShaMapLeaf(index, item.copy()); + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapNode.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapNode.java new file mode 100644 index 0000000000..c5e6d00aca --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapNode.java @@ -0,0 +1,47 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.HalfSha512; +import com.ripple.core.coretypes.hash.Hash256; +import com.ripple.core.coretypes.hash.prefixes.Prefix; +import com.ripple.core.serialized.BytesSink; + +abstract public class ShaMapNode { + protected Hash256 hash; + + // This saves a lot of instanceof checks + public abstract boolean isLeaf(); + public abstract boolean isInner(); + + public ShaMapLeaf asLeaf() { + return (ShaMapLeaf) this; + } + public ShaMapInner asInner() { + return (ShaMapInner) this; + } + + abstract Prefix hashPrefix(); + abstract public void toBytesSink(BytesSink sink); + + public void invalidate() {hash = null;} + public Hash256 hash() { + if (hash == null) { + hash = createHash(); + } + return hash; + } + private Hash256 createHash() { + HalfSha512 half = HalfSha512.prefixed256(hashPrefix()); + toBytesSink(half); + return half.finish(); + } + /** + * Walk any leaves, possibly this node itself, if it's terminal. + */ + public void walkAnyLeaves(LeafWalker leafWalker) { + if (isLeaf()) { + leafWalker.onLeaf(asLeaf()); + } else { + asInner().walkLeaves(leafWalker); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultItem.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultItem.java new file mode 100644 index 0000000000..9e4d34a39d --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultItem.java @@ -0,0 +1,38 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.hash.prefixes.Prefix; +import com.ripple.core.serialized.BinarySerializer; +import com.ripple.core.serialized.BytesSink; +import com.ripple.core.types.known.tx.result.TransactionResult; + +public class TransactionResultItem extends ShaMapItem { + public TransactionResult result; + + public TransactionResultItem(TransactionResult result) { + this.result = result; + } + + @Override + void toBytesSink(BytesSink sink) { + BinarySerializer write = new BinarySerializer(sink); + write.addLengthEncoded(result.txn); + write.addLengthEncoded(result.meta); + } + + @Override + public ShaMapItem copy() { + // that's ok right ;) these bad boys are immutable anyway + return this; + } + + @Override + public TransactionResult value() { + return result; + } + + @Override + public Prefix hashPrefix() { + return HashPrefix.txNode; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultVisitor.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultVisitor.java new file mode 100644 index 0000000000..3993b3795d --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultVisitor.java @@ -0,0 +1,6 @@ +package com.ripple.core.types.shamap; +import com.ripple.core.types.known.tx.result.TransactionResult; + +public interface TransactionResultVisitor { + public void onTransaction(TransactionResult tx); +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionTree.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionTree.java new file mode 100644 index 0000000000..72c4ac4ddc --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionTree.java @@ -0,0 +1,43 @@ +package com.ripple.core.types.shamap; + +import com.ripple.core.types.known.tx.result.TransactionResult; + +import java.util.TreeSet; + +public class TransactionTree extends ShaMap { + public TransactionTree() { + super(); + } + + public TransactionTree(boolean isCopy, int depth) { + super(isCopy, depth); + } + + @Override + protected ShaMapInner makeInnerOfSameClass(int depth) { + return new TransactionTree(true, depth); + } + + public void addTransactionResult(TransactionResult tr) { + TransactionResultItem item = new TransactionResultItem(tr); + addItem(tr.hash, item); + } + + @Override + public TransactionTree copy() { + return (TransactionTree) super.copy(); + } + + public void walkTransactions(final TransactionResultVisitor walker) { + walkLeaves(leaf -> { + TransactionResultItem item = (TransactionResultItem) leaf.item; + walker.onTransaction(item.result); + }); + } + + public TreeSet toTreeSet() { + TreeSet result = new TreeSet<>(); + walkTransactions(result::add); + return result; + } +} diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/TreeWalker.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/TreeWalker.java new file mode 100644 index 0000000000..de225e9eaf --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/TreeWalker.java @@ -0,0 +1,6 @@ +package com.ripple.core.types.shamap; + +public interface TreeWalker { + public void onLeaf(ShaMapLeaf leaf); + public void onInner(ShaMapInner inner); +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/Seed.java b/ripple-core/src/main/java/com/ripple/crypto/Seed.java new file mode 100644 index 0000000000..5c0231828f --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/Seed.java @@ -0,0 +1,85 @@ +package com.ripple.crypto; + +import com.ripple.crypto.ecdsa.K256; +import com.ripple.crypto.ed25519.EDKeyPair; +import com.ripple.crypto.keys.IKeyPair; +import com.ripple.encodings.addresses.Addresses; +import com.ripple.encodings.base58.B58; +import com.ripple.utils.Sha512; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +public class Seed { + private final byte[] seedBytes; + private B58.Version version; + + public Seed(byte[] seedBytes) { + this(Addresses.SEED_K256, seedBytes); + } + + public Seed(B58.Version version, byte[] seedBytes) { + this.seedBytes = seedBytes; + this.version = version; + } + + @Override + public String toString() { + return Addresses.encode(seedBytes, version); + } + + public byte[] bytes() { + return seedBytes; + } + + public B58.Version version() { + return version; + } + + public Seed setEd25519() { + this.version = Addresses.SEED_ED25519; + return this; + } + + public IKeyPair keyPair() { + return keyPair(0); + } + + public IKeyPair rootKeyPair() { + return keyPair(-1); + } + + public IKeyPair keyPair(int account) { + if (version == Addresses.SEED_ED25519 || + Arrays.equals(version.bytes, Addresses.SEED_ED25519.bytes)) { + if (account != 0) throw new IllegalStateException(); + return EDKeyPair.from128Seed(seedBytes); + } else { + return K256.createKeyPair(seedBytes, account); + } + + } + + public static Seed fromBase58(String b58) { + B58.Decoded decoded = Addresses.decodeSeed(b58); + return new Seed(decoded.version, decoded.payload); + } + + public static Seed fromPassPhrase(String passPhrase) { + return new Seed(passPhraseToSeedBytes(passPhrase)); + } + + public static byte[] passPhraseToSeedBytes(String phrase) { + try { + return new Sha512(phrase.getBytes("utf-8")).finish128(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public static IKeyPair getKeyPair(String b58) { + return fromBase58(b58).keyPair(); + } +} + + diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/ECDSASignature.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/ECDSASignature.java new file mode 100644 index 0000000000..ab61bfec30 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/ECDSASignature.java @@ -0,0 +1,155 @@ +package com.ripple.crypto.ecdsa; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.DERSequenceGenerator; +import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.crypto.signers.ECDSASigner; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; + +public class ECDSASignature { + /** The two components of the signature. */ + public BigInteger r, s; + + /** Constructs a signature with the given components. */ + public ECDSASignature(BigInteger r, BigInteger s) { + this.r = r; + this.s = s; + } + + public static boolean isStrictlyCanonical(byte[] sig) { + return checkIsCanonical(sig, true); + } + + public static boolean checkIsCanonical(byte[] sig, boolean strict) { + // Make sure signature is canonical + // To protect against signature morphing attacks + + // Signature should be: + // <30> [ <02> ] [ <02> ] + // where + // 6 <= len <= 70 + // 1 <= lenR <= 33 + // 1 <= lenS <= 33 + + int sigLen = sig.length; + + if ((sigLen < 8) || (sigLen > 72)) + return false; + + if ((sig[0] != 0x30) || (sig[1] != (sigLen - 2))) + return false; + + // Find R and check its length + int rPos = 4, rLen = sig[rPos - 1]; + + if ((rLen < 1) || (rLen > 33) || ((rLen + 7) > sigLen)) + return false; + + // Find S and check its length + int sPos = rLen + 6, sLen = sig[sPos - 1]; + if ((sLen < 1) || (sLen > 33) || ((rLen + sLen + 6) != sigLen)) + return false; + + if ((sig[rPos - 2] != 0x02) || (sig[sPos - 2] != 0x02)) + return false; // R or S have wrong type + + if ((sig[rPos] & 0x80) != 0) + return false; // R is negative + + if ((sig[rPos] == 0) && rLen == 1) + return false; // R is zero + + if ((sig[rPos] == 0) && ((sig[rPos + 1] & 0x80) == 0)) + return false; // R is padded + + if ((sig[sPos] & 0x80) != 0) + return false; // S is negative + + if ((sig[sPos] == 0) && sLen == 1) + return false; // S is zero + + if ((sig[sPos] == 0) && ((sig[sPos + 1] & 0x80) == 0)) + return false; // S is padded + + + byte[] rBytes = new byte[rLen]; + byte[] sBytes = new byte[sLen]; + + System.arraycopy(sig, rPos, rBytes, 0, rLen); + System.arraycopy(sig, sPos, sBytes, 0, sLen); + + BigInteger r = new BigInteger(1, rBytes), s = new BigInteger(1, sBytes); + + BigInteger order = SECP256K1.order(); + + if (r.compareTo(order) != -1 || s.compareTo(order) != -1) { + return false; // R or S greater than modulus + } + if (strict) { + return order.subtract(s).compareTo(s) != -1; + } else { + return true; + } + + } + + public static ECDSASignature createSignature(byte[] hash, ECDSASigner signer) { + BigInteger[] sigs = signer.generateSignature(hash); + BigInteger r = sigs[0], s = sigs[1]; + + BigInteger otherS = SECP256K1.order().subtract(s); + if (s.compareTo(otherS) > 0) { + s = otherS; + } + + return new ECDSASignature(r, s); + } + + /** + * DER is an international standard for serializing data structures which is widely used in cryptography. + * It's somewhat like protocol buffers but less convenient. This method returns a standard DER encoding + * of the signature, as recognized by OpenSSL and other libraries. + */ + public byte[] encodeToDER() { + try { + return derByteStream().toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); // Cannot happen. + } + } + + public static ECDSASignature decodeFromDER(byte[] bytes) { + try { + ASN1InputStream decoder = new ASN1InputStream(bytes); + DLSequence seq = (DLSequence) decoder.readObject(); + ASN1Integer r, s; + try { + r = (ASN1Integer) seq.getObjectAt(0); + s = (ASN1Integer) seq.getObjectAt(1); + } catch (ClassCastException e) { + return null; + } finally { + decoder.close(); + } + // OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be + // Thus, we always use the positive versions. See: http://r6.ca/blog/20111119T211504Z.html + return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected ByteArrayOutputStream derByteStream() throws IOException { + // Usually 70-72 bytes. + ByteArrayOutputStream bos = new ByteArrayOutputStream(72); + DERSequenceGenerator seq = new DERSequenceGenerator(bos); + seq.addObject(new ASN1Integer(r)); + seq.addObject(new ASN1Integer(s)); + seq.close(); + return bos; + } +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256.java new file mode 100644 index 0000000000..1f93f23465 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256.java @@ -0,0 +1,103 @@ +package com.ripple.crypto.ecdsa; + +import com.ripple.utils.Sha512; +import com.ripple.utils.Utils; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.math.ec.ECPoint; + +import java.math.BigInteger; + +public class K256 { + + /** + * @param secretKey secret point on the curve as BigInteger + * @return corresponding public point + */ + public static byte[] getPublic(BigInteger secretKey) { + return SECP256K1.basePointMultipliedBy(secretKey); + } + + /** + * @param secretKey secret point on the curve as BigInteger + * @return corresponding public point + */ + private static ECPoint computePublic(BigInteger secretKey) { + return SECP256K1.basePoint().multiply(secretKey); + } + + /** + * @param privateGen secret point on the curve as BigInteger + * @return the corresponding public key is the public generator + * (aka public root key, master public key). + * return as byte[] for convenience. + */ + private static ECPoint computePublicGenerator(BigInteger privateGen) { + return computePublic(privateGen); + } + + private static BigInteger computePrivateGen(byte[] seedBytes) { + return generateKey(seedBytes, null); + } + + public static byte[] computePublicKey(byte[] publicGenBytes, + int accountNumber) { + ECPoint rootPubPoint = SECP256K1.curve().decodePoint(publicGenBytes); + BigInteger scalar = generateKey(publicGenBytes, accountNumber); + ECPoint point = SECP256K1.basePoint().multiply(scalar); + ECPoint offset = rootPubPoint.add(point); + return offset.getEncoded(true); + } + + private static BigInteger computeSecretKey(BigInteger privateGen, + byte[] publicGenBytes, + int accountNumber) { + return generateKey(publicGenBytes, accountNumber) + .add(privateGen).mod(SECP256K1.order()); + } + + /** + * @param seedBytes - a bytes sequence of arbitrary length which will be hashed + * @param discriminator - nullable optional uint32 to hash + * @return a number between [1, order -1] suitable as a private key + */ + private static BigInteger generateKey(byte[] seedBytes, Integer discriminator) { + BigInteger key = null; + for (long i = 0; i <= 0xFFFFFFFFL; i++) { + Sha512 sha512 = new Sha512().add(seedBytes); + if (discriminator != null) { + sha512.addU32(discriminator); + } + sha512.addU32((int) i); + byte[] keyBytes = sha512.finish256(); + key = Utils.uBigInt(keyBytes); + if (key.compareTo(BigInteger.ZERO) > 0 && + key.compareTo(SECP256K1.order()) < 0) { + break; + } + } + return key; + } + + static ECDSASignature createECDSASignature(byte[] hash, ECDSASigner signer) { + return ECDSASignature.createSignature(hash, signer); + } + + public static K256KeyPair createKeyPair(byte[] seedBytes, int accountNumber) { + @SuppressWarnings("SpellCheckingInspection") + BigInteger priv; + BigInteger privateGen; + // The private generator (aka root private key, master private key) + privateGen = computePrivateGen(seedBytes); + ECPoint publicGen = computePublicGenerator(privateGen); + byte[] pubGenBytes = publicGen.getEncoded(true); + + if (accountNumber == -1) { + // The root keyPair + return new K256KeyPair(privateGen, publicGen, pubGenBytes); + } else { + priv = computeSecretKey(privateGen, pubGenBytes, accountNumber); + ECPoint pub = computePublic(priv); + return new K256KeyPair(priv, pub, null); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256KeyPair.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256KeyPair.java new file mode 100644 index 0000000000..e417c9d85c --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256KeyPair.java @@ -0,0 +1,54 @@ +package com.ripple.crypto.ecdsa; + +import com.ripple.crypto.keys.IKeyPair; +import com.ripple.utils.HashUtils; +import com.ripple.utils.Utils; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.bouncycastle.math.ec.ECPoint; + +import java.math.BigInteger; + +public class K256KeyPair extends K256VerifyingKey implements IKeyPair { + private byte[] privateKey; + private ECPrivateKeyParameters privateKeyParameters; + + K256KeyPair(BigInteger privateKey, ECPoint pub, byte[] pubEncoded) { + super(pub, pubEncoded); + this.privateKey = Utils.padTo256(privateKey.toByteArray()); + privateKeyParameters = new ECPrivateKeyParameters(privateKey, + SECP256K1.params()); + } + + @Override + public byte[] privateKey() { + return privateKey; + } + + @Override + public byte[] signMessage(byte[] message) { + byte[] hash = HashUtils.halfSha512(message); + return signHash(hash); + } + + public byte[] signHash(byte[] bytes) { + ECDSASigner signer = newSigner(); + + ECDSASignature sig = K256.createECDSASignature(bytes, signer); + byte[] der = sig.encodeToDER(); + if (!ECDSASignature.isStrictlyCanonical(der)) { + throw new IllegalStateException("Signature is not strictly canonical"); + } + return der; + } + + private ECDSASigner newSigner() { + ECDSASigner signer = new ECDSASigner( + new HMacDSAKCalculator(new SHA256Digest())); + signer.init(true, privateKeyParameters); + return signer; + } + +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256VerifyingKey.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256VerifyingKey.java new file mode 100644 index 0000000000..7681c12ef6 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256VerifyingKey.java @@ -0,0 +1,55 @@ +package com.ripple.crypto.ecdsa; + +import com.ripple.crypto.keys.IVerifyingKey; +import com.ripple.encodings.addresses.Addresses; +import com.ripple.utils.HashUtils; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.math.ec.ECPoint; + +public class K256VerifyingKey implements IVerifyingKey { + private final ECPublicKeyParameters keyParameters; + private final byte[] canonicalPublicKey; + + K256VerifyingKey(ECPoint publicKey, byte[] publicKeyBytes) { + if (publicKeyBytes == null) { + publicKeyBytes = publicKey.getEncoded(true); + } else if (publicKey == null) { + publicKey = SECP256K1.curve().decodePoint(publicKeyBytes); + } + canonicalPublicKey = publicKeyBytes; + keyParameters = new ECPublicKeyParameters( + publicKey, SECP256K1.params()); + } + + private K256VerifyingKey(byte[] pubKeyBytes) { + this(null, pubKeyBytes); + } + + @Override + public byte[] canonicalPubBytes() { + return canonicalPublicKey; + } + + @Override + public boolean verify(byte[] message, byte[] signature) { + byte[] bytes = HashUtils.halfSha512(message); + return verifyHash(bytes, signature); + } + + public boolean verifyHash(byte[] hash, byte[] signature) { + ECDSASigner signer = new ECDSASigner(); + signer.init(false, keyParameters); + ECDSASignature sig = ECDSASignature.decodeFromDER(signature); + return sig != null && signer.verifySignature(hash, sig.r, sig.s); + } + + public static K256VerifyingKey fromNodePublicKey(String node) { + byte[] pub = Addresses.decodeNodePublic(node); + return new K256VerifyingKey(pub); + } + + public static K256VerifyingKey fromCanonicalPubBytes(byte[] bytes) { + return new K256VerifyingKey(bytes); + } +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/SECP256K1.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/SECP256K1.java new file mode 100644 index 0000000000..5350a27f75 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/SECP256K1.java @@ -0,0 +1,42 @@ +package com.ripple.crypto.ecdsa; + +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.math.ec.ECPoint; + +import java.math.BigInteger; + +public class SECP256K1 { + private static final ECDomainParameters ecParams; + private static final X9ECParameters params; + + static { + + params = SECNamedCurves.getByName("secp256k1"); + ecParams = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + } + + public static ECDomainParameters params() { + return ecParams; + } + + public static BigInteger order() { + return ecParams.getN(); + } + + + public static ECCurve curve() { + return ecParams.getCurve(); + } + + public static ECPoint basePoint() { + return ecParams.getG(); + } + + static byte[] basePointMultipliedBy(BigInteger secret) { + return basePoint().multiply(secret).getEncoded(true); + } + +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/ed25519/ED25519.java b/ripple-core/src/main/java/com/ripple/crypto/ed25519/ED25519.java new file mode 100644 index 0000000000..71da658b0c --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/ed25519/ED25519.java @@ -0,0 +1,9 @@ +package com.ripple.crypto.ed25519; + +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; + +public class ED25519 { + static final EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable + .getByName("Ed25519"); +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDKeyPair.java b/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDKeyPair.java new file mode 100644 index 0000000000..5d638617c3 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDKeyPair.java @@ -0,0 +1,52 @@ +package com.ripple.crypto.ed25519; + +import com.ripple.crypto.keys.IKeyPair; +import com.ripple.utils.HashUtils; +import com.ripple.utils.Utils; +import net.i2p.crypto.eddsa.EdDSAEngine; +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; + +import java.math.BigInteger; +import java.security.PrivateKey; + +public class EDKeyPair extends EDVerifyingKey implements IKeyPair { + + private final EdDSAPrivateKeySpec keySpec; + + private EDKeyPair(EdDSAPrivateKeySpec keySpec) { + super(new EdDSAPublicKeySpec(keySpec.getA(), ED25519.ed25519), null); + this.keySpec = keySpec; + } + + public static EDKeyPair from256Seed(byte[] seedBytes) { + EdDSAPrivateKeySpec keySpec = new EdDSAPrivateKeySpec(seedBytes, + ED25519.ed25519); + return new EDKeyPair(keySpec); + } + + public static EDKeyPair from128Seed(byte[] seedBytes) { + assert seedBytes.length == 16; + return from256Seed(HashUtils.halfSha512(seedBytes)); + } + + @Override + public byte[] privateKey() { + return keySpec.geta(); + } + + @Override + public byte[] signMessage(byte[] message) { + try { + EdDSAEngine sgr = new EdDSAEngine(sha512digest()); + PrivateKey sKey = new EdDSAPrivateKey(keySpec); + sgr.initSign(sKey); + sgr.update(message); + return sgr.sign(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDVerifyingKey.java b/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDVerifyingKey.java new file mode 100644 index 0000000000..e0c9da8d07 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDVerifyingKey.java @@ -0,0 +1,58 @@ +package com.ripple.crypto.ed25519; + +import com.ripple.crypto.keys.IVerifyingKey; +import net.i2p.crypto.eddsa.EdDSAEngine; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; +import org.bouncycastle.util.Arrays; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class EDVerifyingKey implements IVerifyingKey { + private final EdDSAPublicKey publicKey; + private final byte[] canonicalPubBytes; + @SuppressWarnings("FieldCanBeLocal") + private final byte[] ED_PREFIX = {(byte) 0xED}; + + EDVerifyingKey(EdDSAPublicKeySpec spec, byte[] pubBytes) { + if (pubBytes == null) { + pubBytes = spec.getA().toByteArray(); + } else if (spec == null) { + spec = new EdDSAPublicKeySpec(pubBytes, ED25519.ed25519); + } + publicKey = new EdDSAPublicKey(spec); + canonicalPubBytes = Arrays.concatenate(ED_PREFIX, pubBytes); + } + + MessageDigest sha512digest() { + try { + return MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] canonicalPubBytes() { + return canonicalPubBytes; + } + + @Override + public boolean verify(byte[] message, byte[] sigBytes) { + try { + EdDSAEngine sgr = new EdDSAEngine(sha512digest()); + sgr.initVerify(publicKey); + sgr.update(message); + return sgr.verify(sigBytes); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static EDVerifyingKey fromCanonicalPubBytes(byte[] bytes) { + // Strip the 0xED prefix from the key + byte[] pubBytes = Arrays.copyOfRange(bytes, 1, bytes.length); + return new EDVerifyingKey(null, pubBytes); + } +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/keys/IKeyPair.java b/ripple-core/src/main/java/com/ripple/crypto/keys/IKeyPair.java new file mode 100644 index 0000000000..ab76848154 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/keys/IKeyPair.java @@ -0,0 +1,6 @@ +package com.ripple.crypto.keys; + +public interface IKeyPair extends IVerifyingKey { + byte[] privateKey(); + byte[] signMessage(byte[] message); +} diff --git a/ripple-core/src/main/java/com/ripple/crypto/keys/IVerifyingKey.java b/ripple-core/src/main/java/com/ripple/crypto/keys/IVerifyingKey.java new file mode 100644 index 0000000000..1b1c6ed7d0 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/crypto/keys/IVerifyingKey.java @@ -0,0 +1,29 @@ +package com.ripple.crypto.keys; + + +import com.ripple.crypto.ecdsa.K256VerifyingKey; +import com.ripple.crypto.ed25519.EDVerifyingKey; +import com.ripple.encodings.common.B16; +import com.ripple.utils.HashUtils; + +public interface IVerifyingKey { + default String canonicalPubHex() { + return B16.encode(canonicalPubBytes()); + } + + default byte[] id() { + return HashUtils.SHA256_RIPEMD160(canonicalPubBytes()); + } + + byte[] canonicalPubBytes(); + + boolean verify(byte[] message, byte[] sigBytes); + + static IVerifyingKey from(byte[] bytes) { + if (bytes[0] == (byte) 0xED) { + return EDVerifyingKey.fromCanonicalPubBytes(bytes); + } else { + return K256VerifyingKey.fromCanonicalPubBytes(bytes); + } + } +} \ No newline at end of file diff --git a/ripple-core/src/main/java/com/ripple/encodings/addresses/Addresses.java b/ripple-core/src/main/java/com/ripple/encodings/addresses/Addresses.java new file mode 100644 index 0000000000..5d8022a9b6 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/encodings/addresses/Addresses.java @@ -0,0 +1,77 @@ +package com.ripple.encodings.addresses; + + +import com.ripple.encodings.base58.B58; +import com.ripple.encodings.basex.EncodingFormatException; +import com.ripple.encodings.basex.IBaseX; +import org.omg.IOP.Encoding; + +public class Addresses { + public static final B58 codec = new B58("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"); + + // Otherwise known as `family seed` + public static final B58.Version SEED_K256 = new B58.Version( + 33, "seedK256", 16); + + public static final B58.Version SEED_ED25519 = new B58.Version( + "01E14B", "seedEd25519", 16); + + public static final B58.Version ACCOUNT_ID = new B58.Version( + 0, "accountId", 20); + + public static final B58.Version NODE_PUBLIC_KEY = new B58.Version( + 28, "nodePublicKey", 33); + + public static final B58.Version NODE_PRIVATE_KEY = new B58.Version( + 33, "nodePrivateKey", 32); + + public static byte[] decode(String encoded, B58.Version version) { + return codec.decodeVersioned(encoded, version).payload; + } + + public static String encode(byte[] bytes, B58.Version version) { + return codec.encodeVersioned(bytes, version); + } + + public static byte[] decodeSeedToBytes(String seed) { + return codec.decodeVersioned(seed, SEED_K256, SEED_ED25519) + .payload; + } + + public static B58.Decoded decodeSeed(String seed) { + return codec.decodeVersioned(seed, SEED_K256, SEED_ED25519); + } + + public static String encodeSeedK256(byte[] bytes) { + return encode(bytes, SEED_K256); + } + + public static String encodeAccountID(byte[] bytes) { + return encode(bytes, ACCOUNT_ID); + } + + public static byte[] decodeAccountID(String id) { + return decode(id, ACCOUNT_ID); + } + + public static String encodeNodePublic(byte[] bytes) { + return encode(bytes, NODE_PUBLIC_KEY); + } + + public static byte[] decodeNodePublic(String base58) { + return decode(base58, NODE_PUBLIC_KEY); + } + + public static boolean isValid(String encoded, B58.Version... versions) { + try { + codec.decodeVersioned(encoded, versions); + return true; + } catch (EncodingFormatException e) { + return false; + } + } + + public static boolean isValidAccountID(String encoded) { + return isValid(encoded, ACCOUNT_ID); + } +} diff --git a/ripple-core/src/main/java/com/ripple/encodings/base58/B58.java b/ripple-core/src/main/java/com/ripple/encodings/base58/B58.java new file mode 100644 index 0000000000..a6b928824e --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/encodings/base58/B58.java @@ -0,0 +1,9 @@ +package com.ripple.encodings.base58; + +import com.ripple.encodings.basex.BaseX; + +public class B58 extends BaseX { + public B58(String alphabet) { + super(alphabet); + } +} diff --git a/ripple-core/src/main/java/com/ripple/encodings/basex/BaseX.java b/ripple-core/src/main/java/com/ripple/encodings/basex/BaseX.java new file mode 100644 index 0000000000..4ebbfc871a --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/encodings/basex/BaseX.java @@ -0,0 +1,231 @@ +package com.ripple.encodings.basex; + +import com.ripple.utils.HashUtils; + +import java.util.Arrays; + +public class BaseX implements IBaseX { + private char[] alphabet; + private final char encodedZero; + protected final int[] indexes; + + public BaseX(String alphabet_) { + if (alphabet_.length() > 256) { + throw new IllegalArgumentException(); + } + alphabet = alphabet_.toCharArray(); + indexes = new int[128]; + encodedZero = alphabet[0]; + Arrays.fill(indexes, -1); + for (int i = 0; i < alphabet.length; i++) { + indexes[alphabet[i]] = i; + } + } + + private static String repeat(int times, char repeated) { + char[] chars = new char[times]; + Arrays.fill(chars, repeated); + return new String(chars); + } + + /** + * Encodes the given bytes as a base58 string (no checksum is appended). + * + * @param input the bytes to encode + * @return the base58-encoded string + */ + @Override + public String encode(byte[] input) { + if (input.length == 0) { + return ""; + } + // Count leading zeros. + int zeros = 0; + while (zeros < input.length && input[zeros] == 0) { + ++zeros; + } + // Convert base-256 digits to base-58 digits (plus conversion to ASCII characters) + input = Arrays.copyOf(input, input.length); // since we modify it in-place + char[] encoded = new char[input.length * 2]; // upper bound + int outputStart = encoded.length; + for (int inputStart = zeros; inputStart < input.length; ) { + encoded[--outputStart] = alphabet[divmod(input, inputStart, 256, 58)]; + if (input[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input. + while (outputStart < encoded.length && encoded[outputStart] == encodedZero) { + ++outputStart; + } + while (--zeros >= 0) { + encoded[--outputStart] = encodedZero; + } + // Return encoded string (including encoded leading zeros). + return new String(encoded, outputStart, encoded.length - outputStart); + } + + /** + * Decodes the given base58 string into the original data bytes. + * + * @param input the base58-encoded string to decode + * @return the decoded data bytes + * @throws EncodingFormatException if the given string is not a valid base58 string + */ + @Override + public byte[] decode(String input) throws EncodingFormatException { + if (input.length() == 0) { + return new byte[0]; + } + // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits). + byte[] input58 = new byte[input.length()]; + for (int i = 0; i < input.length(); ++i) { + char c = input.charAt(i); + int digit = c < 128 ? indexes[c] : -1; + if (digit < 0) { + throw new EncodingFormatException("Illegal character " + c + " at position " + i); + } + input58[i] = (byte) digit; + } + // Count leading zeros. + int zeros = 0; + while (zeros < input58.length && input58[zeros] == 0) { + ++zeros; + } + // Convert base-58 digits to base-256 digits. + byte[] decoded = new byte[input.length()]; + int outputStart = decoded.length; + for (int inputStart = zeros; inputStart < input58.length; ) { + decoded[--outputStart] = divmod(input58, inputStart, 58, 256); + if (input58[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Ignore extra leading zeroes that were added during the calculation. + while (outputStart < decoded.length && decoded[outputStart] == 0) { + ++outputStart; + } + // Return decoded data (including original number of leading zeros). + return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length); + } + + /** + * Decodes the given base58 string into the original data bytes, using the checksum in the + * last 4 bytes of the decoded data to verify that the rest are correct. The checksum is + * removed from the returned data. + * + * @param input the base58-encoded string to decode (which should include the checksum) + * @throws EncodingFormatException if the input is not base 58 or the checksum does not validate. + */ + public byte[] decodeChecked(String input) throws EncodingFormatException { + byte[] decoded = decode(input); + if (decoded.length < 4) + throw new EncodingFormatException("Input too short"); + byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4); + byte[] checksum = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length); + byte[] actualChecksum = Arrays.copyOfRange(HashUtils.doubleDigest(data), 0, 4); + if (!Arrays.equals(checksum, actualChecksum)) + throw new EncodingFormatException("Checksum does not validate"); + return data; + } + + /** + * Divides a number, represented as an array of bytes each containing a single digit + * in the specified base, by the given divisor. The given number is modified in-place + * to contain the quotient, and the return value is the remainder. + * + * @param number the number to divide + * @param firstDigit the index within the array of the first non-zero digit + * (this is used for optimization by skipping the leading zeros) + * @param base the base in which the number's digits are represented (up to 256) + * @param divisor the number to divide by (up to 256) + * @return the remainder of the division operation + */ + private byte divmod(byte[] number, int firstDigit, int base, int divisor) { + // this is just long division which accounts for the base of the input digits + int remainder = 0; + for (int i = firstDigit; i < number.length; i++) { + int digit = (int) number[i] & 0xFF; + int temp = remainder * base + digit; + number[i] = (byte) (temp / divisor); + remainder = temp % divisor; + } + return (byte) remainder; + } + + @Override + public byte[] findPrefix(int payLoadLength, String desiredPrefix) { + if (alphabet.length != 58) { + throw new IllegalStateException("Must be base58"); + } + int totalLength = payLoadLength + 4; // for the checksum + double chars = Math.log(Math.pow(256, totalLength)) / Math.log(58); + int requiredChars = (int) Math.ceil(chars + 0.2D); + // Mess with this to see stability tests fail + int charPos = (alphabet.length / 2) - 1; + char padding = alphabet[(charPos)]; + String template = desiredPrefix + repeat(requiredChars, padding); + byte[] decoded = decode(template); + return copyOfRange(decoded, 0, decoded.length - totalLength); + } + + @Override + public String encodeVersioned(byte[] input, Version version) { + if (input.length != version.expectedLength) { + throw new IllegalArgumentException( + "input length=" + input.length + + ", expected=" + version.expectedLength); + } + return encode(concatVersionAndAddChecksum(input, version.bytes)); + } + + private byte[] concatVersionAndAddChecksum(byte[] input, byte[] version) { + byte[] buffer = new byte[input.length + version.length]; + System.arraycopy(version, 0, buffer, 0, version.length); + System.arraycopy(input, 0, buffer, version.length, input.length); + byte[] checkSum = copyOfRange(HashUtils.doubleDigest(buffer), 0, 4); + byte[] output = new byte[buffer.length + checkSum.length]; + System.arraycopy(buffer, 0, output, 0, buffer.length); + System.arraycopy(checkSum, 0, output, buffer.length, checkSum.length); + return output; + } + + public Decoded decodeVersioned(String input, + Version... possibleVersions) throws EncodingFormatException { + + byte[] buffer = decodeChecked(input); + + Version foundVersion = null; + int expectedLength = possibleVersions[0].expectedLength; + int versionLength = buffer.length - expectedLength; + byte[] versionBytes = copyOfRange(buffer, 0, versionLength); + + for (Version possible : possibleVersions) { + if (possible.expectedLength != expectedLength) { + throw new IllegalStateException(); + } + + if (Arrays.equals(possible.bytes, versionBytes)) { + foundVersion = possible; + break; + } + } + if (foundVersion == null) { + throw new EncodingFormatException("Incorrect version"); + } + + byte[] bytes = copyOfRange(buffer, versionLength, buffer.length); + if (bytes.length != expectedLength) { + throw new EncodingFormatException("Incorrect length"); + } + + return new Decoded(foundVersion, bytes); + } + + private byte[] copyOfRange(byte[] source, int from, int to) { + byte[] range = new byte[to - from]; + System.arraycopy(source, from, range, 0, range.length); + return range; + } + +} \ No newline at end of file diff --git a/ripple-core/src/main/java/com/ripple/encodings/basex/EncodingFormatException.java b/ripple-core/src/main/java/com/ripple/encodings/basex/EncodingFormatException.java new file mode 100644 index 0000000000..4c56ffa1e9 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/encodings/basex/EncodingFormatException.java @@ -0,0 +1,7 @@ +package com.ripple.encodings.basex; + +public class EncodingFormatException extends RuntimeException{ + public EncodingFormatException(String message) { + super(message); + } +} diff --git a/ripple-core/src/main/java/com/ripple/encodings/basex/IBaseX.java b/ripple-core/src/main/java/com/ripple/encodings/basex/IBaseX.java new file mode 100644 index 0000000000..84d98aa51b --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/encodings/basex/IBaseX.java @@ -0,0 +1,41 @@ +package com.ripple.encodings.basex; + +import com.ripple.encodings.common.B16; + +public interface IBaseX { + String encode(byte[] input); + byte[] decode(String input); + + byte[] findPrefix(int payLoadLength, String desiredPrefix); + + String encodeVersioned(byte[] input, Version version); + Decoded decodeVersioned(String input, Version... possibleVersions); + + class Decoded { + public final Version version; + public final byte[] payload; + + Decoded(Version version, byte[] payload) { + this.version = version; + this.payload = payload; + } + } + + class Version { + public final byte[] bytes; + public final String name; + public final int expectedLength; + + public Version(byte[] bytes, String name, int length) { + this.bytes = bytes; + this.name = name; + this.expectedLength = length; + } + public Version(int aByte, String name, int length) { + this(new byte[]{(byte) aByte}, name, length); + } + public Version(String hex, String name, int length) { + this(B16.decode(hex), name, length); + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/encodings/common/B16.java b/ripple-core/src/main/java/com/ripple/encodings/common/B16.java new file mode 100644 index 0000000000..9ab895721c --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/encodings/common/B16.java @@ -0,0 +1,20 @@ +package com.ripple.encodings.common; + + +import static org.bouncycastle.util.encoders.Hex.toHexString; + +public class B16 { + public static String toStringTrimmed(byte[] bytes) { + int offset = 0; + if (bytes[0] == 0) { + offset = 1; + } + return toHexString(bytes, offset, bytes.length - offset).toUpperCase(); + } + public static String encode(byte[] bytes) { + return toHexString(bytes).toUpperCase(); + } + public static byte[] decode(String hex) { + return org.bouncycastle.util.encoders.Hex.decode(hex); + } +} diff --git a/ripple-core/src/main/java/com/ripple/encodings/common/B64.java b/ripple-core/src/main/java/com/ripple/encodings/common/B64.java new file mode 100644 index 0000000000..e48b7d1a70 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/encodings/common/B64.java @@ -0,0 +1,12 @@ +package com.ripple.encodings.common; + +import org.bouncycastle.util.encoders.Base64; + +public class B64 { + public static String toString(byte[] bytes) { + return Base64.toBase64String(bytes); + } + public static byte[] decode(String string) { + return Base64.decode(string); + } +} diff --git a/ripple-core/src/main/java/com/ripple/utils/HashUtils.java b/ripple-core/src/main/java/com/ripple/utils/HashUtils.java new file mode 100644 index 0000000000..2d79986957 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/utils/HashUtils.java @@ -0,0 +1,62 @@ +package com.ripple.utils; + +import org.bouncycastle.crypto.digests.RIPEMD160Digest; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class HashUtils { + private static final MessageDigest digest; + static { + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); // Can't happen. + } + } + + /** + * See {@link HashUtils#doubleDigest(byte[], int, int)}. + */ + public static byte[] doubleDigest(byte[] input) { + return doubleDigest(input, 0, input.length); + } + + /** + * Calculates the SHA-256 hash of the given byte range, and then hashes the resulting hash again. This is + * standard procedure in Bitcoin. The resulting hash is in big endian form. + */ + public static byte[] doubleDigest(byte[] input, int offset, int length) { + synchronized (digest) { + digest.reset(); + digest.update(input, offset, length); + byte[] first = digest.digest(); + return digest.digest(first); + } + } + + public static byte[] halfSha512(byte[] bytes) { + return new Sha512(bytes).finish256(); + } + + public static byte[] quarterSha512(byte[] bytes) { + return new Sha512(bytes).finish128(); + } + + public static byte[] sha512(byte[] bytes) { + return new Sha512(bytes).finish(); + } + + public static byte[] SHA256_RIPEMD160(byte[] input) { + try { + byte[] sha256 = MessageDigest.getInstance("SHA-256").digest(input); + RIPEMD160Digest digest = new RIPEMD160Digest(); + digest.update(sha256, 0, sha256.length); + byte[] out = new byte[20]; + digest.doFinal(out, 0); + return out; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); // Cannot happen. + } + } +} diff --git a/ripple-core/src/main/java/com/ripple/utils/Sha512.java b/ripple-core/src/main/java/com/ripple/utils/Sha512.java new file mode 100644 index 0000000000..bafd20b134 --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/utils/Sha512.java @@ -0,0 +1,53 @@ +package com.ripple.utils; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public class Sha512 { + private MessageDigest messageDigest; + + public Sha512() { + try { + messageDigest = MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public Sha512(byte[] start) { + this(); + add(start); + } + + public Sha512 add(byte[] bytes) { + messageDigest.update(bytes); + return this; + } + + public Sha512 addU32(int i) { + messageDigest.update((byte) ((i >>> 24) & 0xFF)); + messageDigest.update((byte) ((i >>> 16) & 0xFF)); + messageDigest.update((byte) ((i >>> 8) & 0xFF)); + messageDigest.update((byte) ((i) & 0xFF)); + return this; + } + + private byte[] finishTaking(int size) { + byte[] hash = new byte[size]; + System.arraycopy(messageDigest.digest(), 0, hash, 0, size); + return hash; + } + + public byte[] finish128() { + return finishTaking(16); + } + + public byte[] finish256() { + return finishTaking(32); + } + + public byte[] finish() { + return messageDigest.digest(); + } +} diff --git a/ripple-core/src/main/java/com/ripple/utils/Utils.java b/ripple-core/src/main/java/com/ripple/utils/Utils.java new file mode 100644 index 0000000000..8376f1637f --- /dev/null +++ b/ripple-core/src/main/java/com/ripple/utils/Utils.java @@ -0,0 +1,58 @@ +package com.ripple.utils; + +import com.ripple.encodings.common.B16; + +import java.math.BigInteger; +import java.util.Arrays; + +public class Utils { + public static String bigHex(BigInteger bn) { + return B16.toStringTrimmed(bn.toByteArray()); + } + + public static BigInteger uBigInt(byte[] bytes) { + return new BigInteger(1, bytes); + } + + public static byte[] leadingZeroesTrimmedOrPaddedTo(int size, byte[] bytes) { + if (bytes.length == size) { + return bytes; + } + if (bytes.length > size) { + int length = bytes.length; + int offset = 0; + while (length > size && bytes[offset++] == 0) { + length--; + } + if (length > size) { + throw new IllegalArgumentException( + "bytes.length: " + bytes.length + " > " + + size); + } + return copyFrom(bytes, offset, size); + } + + return padTo(bytes, size); + } + + public static byte[] padTo256(byte[] bytes) { + return leadingZeroesTrimmedOrPaddedTo(32, bytes); + } + + public static byte[] padTo160(byte[] bytes) { + return leadingZeroesTrimmedOrPaddedTo(20, bytes); + } + + private static byte[] copyFrom(byte[] bytes, int offset, int size) { + byte[] result = new byte[size]; + System.arraycopy(bytes, offset, result, 0, size); + return result; + } + + private static byte[] padTo(byte[] bytes, int size) { + byte[] result = new byte[size]; + int offset = result.length - bytes.length; + System.arraycopy(bytes, 0, result, offset, bytes.length); + return result; + } +} diff --git a/settings.gradle b/settings.gradle index ac821b1c6c..7c7add5bef 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':card-android', ':server-android', ':card-common' +include ':app', ':card-android', ':server-android', ':card-common', ':ripple-core'