From 4b4c4af7ae923f3e8fb53eba3d068385f9b13ced Mon Sep 17 00:00:00 2001 From: AZ Date: Sat, 4 Feb 2017 18:56:10 +0100 Subject: [PATCH] multisigs [wip] --- src/main/java/jota/IotaAPI.java | 175 ++++++++++++++- src/main/java/jota/utils/InputValidator.java | 14 ++ src/main/java/jota/utils/Multisig.java | 221 +++++++++++++++++++ 3 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 src/main/java/jota/utils/Multisig.java diff --git a/src/main/java/jota/IotaAPI.java b/src/main/java/jota/IotaAPI.java index 1612e09..db6672d 100644 --- a/src/main/java/jota/IotaAPI.java +++ b/src/main/java/jota/IotaAPI.java @@ -225,7 +225,7 @@ public class IotaAPI extends IotaAPICore { * @param {int} minWeightMagnitude * @return */ - public List sendTrytes(final String[] trytes, final int depth, final int minWeightMagnitude) { + public List sendTrytes(final String[] trytes, final int depth, final int minWeightMagnitude) throws InvalidTrytesException { final GetTransactionsToApproveResponse txs = getTransactionsToApprove(depth); // attach to tangle - do pow @@ -662,7 +662,7 @@ public class IotaAPI extends IotaAPICore { * @method replayBundle * @returns {object} analyzed Transaction objects **/ - public ReplayBundleResponse replayBundle(String transaction, int depth, int minWeightMagnitude) throws InvalidBundleException, ArgumentException, InvalidSignatureException { + public ReplayBundleResponse replayBundle(String transaction, int depth, int minWeightMagnitude) throws InvalidBundleException, ArgumentException, InvalidSignatureException, InvalidTrytesException { StopWatch stopWatch = new StopWatch(); List bundleTrytes = new ArrayList<>(); @@ -706,7 +706,7 @@ public class IotaAPI extends IotaAPICore { return getInclusionStates(hashes, latestMilestone); } - public SendTransferResponse sendTransfer(String seed, int security, int depth, int minWeightMagnitude, final List transfers, Input[] inputs, String address) throws NotEnoughBalanceException, InvalidSecurityLevelException { + public SendTransferResponse sendTransfer(String seed, int security, int depth, int minWeightMagnitude, final List transfers, Input[] inputs, String address) throws NotEnoughBalanceException, InvalidSecurityLevelException, InvalidTrytesException { if (security < 1 || security > 3) { throw new InvalidSecurityLevelException(); @@ -781,6 +781,175 @@ public class IotaAPI extends IotaAPICore { } } + + /** + * Prepares transfer by generating the bundle with the corresponding cosigner transactions + * Does not contain signatures + * + * @param {object} transfers + * @param {function} callback + * @param securitySum sum of security levels used by all co-signers + * @param inputAddress array of input addresses as well as the securitySum + * @param remainderAddress Has to be generated by the cosigners before initiating the transfer, can be null if fully spent + * @method initiateTransfer + * @returns {array} Array of transaction objects + **/ + private GetTransferResponse initiateTransfer(int securitySum, final List inputAddress, String remainderAddress, final List transfers) { + StopWatch sw = new StopWatch(); + + + // If message or tag is not supplied, provide it + // Also remove the checksum of the address if it's there + + for (Transfer transfer : transfers) { + + transfer.setMessage(transfer.getMessage().isEmpty() ? StringUtils.rightPad(transfer.getMessage(), 2187, '9') : transfer.getMessage()); + transfer.setTag(transfer.getTag().isEmpty() ? StringUtils.rightPad(transfer.getTag(), 27, '9') : transfer.getTag()); + + if (Checksum.isValidChecksum(transfer.getAddress())) { + transfer.setAddress(Checksum.removeChecksum(transfer.getAddress())); + } + } + + // Input validation of transfers object + if (!InputValidator.isTransfersCollectionCorrect(transfers)) { + throw new IllegalStateException("Invalid Transfer"); + } + + // validate input address + + for (String address : inputAddress) { + if (!InputValidator.isAddress(address)) + throw new IllegalStateException("Invalid Address"); + } + + // validate remainder address + if (remainderAddress != null && !InputValidator.isAddress(remainderAddress)) { + throw new IllegalStateException("Invalid Address"); + } + + // Create a new bundle + final Bundle bundle = new Bundle(); + + int totalValue = 0; + final List signatureFragments = new ArrayList<>(); + String tag = ""; + // + + // Iterate over all transfers, get totalValue + // and prepare the signatureFragments, message and tag + for (final Transfer transfer : transfers) { + + int signatureMessageLength = 1; + + // If message longer than 2187 trytes, increase signatureMessageLength (add 2nd transaction) + if (transfer.getMessage().length() > 2187) { + + // Get total length, message / maxLength (2187 trytes) + signatureMessageLength += Math.floor(transfer.getMessage().length() / 2187); + + String msgCopy = transfer.getMessage(); + + // While there is still a message, copy it + while (!msgCopy.isEmpty()) { + + String fragment = StringUtils.substring(msgCopy, 0, 2187); + msgCopy = StringUtils.substring(msgCopy, 2187, msgCopy.length()); + + // Pad remainder of fragment + + fragment = StringUtils.rightPad(fragment, 2187, '9'); + + signatureFragments.add(fragment); + } + + } else { + + + // Else, get single fragment with 2187 of 9's trytes + String fragment = StringUtils.substring(transfer.getMessage(), 0, 2187); + + fragment = StringUtils.rightPad(fragment, 2187, '9'); + + signatureFragments.add(fragment); + + } + + // get current timestamp in seconds + long timestamp = (long) Math.floor(Calendar.getInstance().getTimeInMillis() / 1000); + + // If no tag defined, get 27 tryte tag. + tag = transfer.getTag().isEmpty() ? StringUtils.rightPad(transfer.getTag(), 27, '9') : transfer.getTag(); + + // Pad for required 27 tryte length + tag = StringUtils.rightPad(tag, 27, '9'); + + // Add first entry to the bundle + bundle.addEntry(signatureMessageLength, transfer.getAddress(), transfer.getValue(), tag, timestamp); + // Sum up total value + totalValue += transfer.getValue(); + } + + // Get inputs if we are sending tokens + if (totalValue != 0) { + + GetBalancesResponse balancesResponse = getBalances(100, inputAddress); + String[] balances = balancesResponse.getBalances(); + + long totalBalance = 0; + + for (String balance : balances) { + long thisBalance = Integer.parseInt(balance); + + totalBalance += thisBalance; + } + + // get current timestamp in seconds + long timestamp = (long) Math.floor(Calendar.getInstance().getTimeInMillis() / 1000); + + if (totalBalance > 0) { + + long toSubtract = 0 - totalBalance; + + // Add input as bundle entry + // Only a single entry, signatures will be added later + // TODO: fill in the right address + bundle.addEntry(securitySum, "", toSubtract, tag, timestamp); + } + + // Return not enough balance error + if (totalValue > totalBalance) { + throw new IllegalStateException("Not enough balance"); + } + + // If there is a remainder value + // Add extra output to send remaining funds to + if (totalBalance > totalValue) { + + long remainder = totalBalance - totalValue; + + // Remainder bundle entry if necessary + if (remainderAddress == null) { + throw new IllegalStateException("No remainder address defined"); + } + + bundle.addEntry(1, remainderAddress, remainder, tag, timestamp); + } + + + bundle.finalize(customCurl); + bundle.addTrytes(signatureFragments); + + return GetTransferResponse.create(new Bundle[]{}, sw.getElapsedTimeMili()); + + + } else { + + throw new RuntimeException("Invalid value transfer: the transfer does not require a signature."); + } + + } + public String findTailTransactionHash(String hash) throws ArgumentException { GetTrytesResponse gtr = getTrytes(hash); diff --git a/src/main/java/jota/utils/InputValidator.java b/src/main/java/jota/utils/InputValidator.java index 4e01cd4..f3f5be8 100644 --- a/src/main/java/jota/utils/InputValidator.java +++ b/src/main/java/jota/utils/InputValidator.java @@ -27,10 +27,24 @@ public class InputValidator { return trytes.matches("^[A-Z9]{" + (length == 0 ? "0," : length) + "}$"); } + public static boolean isNinesTrytes(final String trytes, final int length) { + return trytes.matches("^[9]{" + (length == 0 ? "0," : length) + "}$"); + } + public static boolean isValue(final String value) { return NumberUtils.isNumber(value); } + public static boolean isArrayOfTrytes(String[] trytes){ + for (String tryte : trytes) { + // Check if correct 2673 trytes + if (!isTrytes(tryte, 2673)) { + return false; + } + } + return true; + } + public static boolean isArrayOfHashes(String[] hashes) { if (hashes == null) return false; diff --git a/src/main/java/jota/utils/Multisig.java b/src/main/java/jota/utils/Multisig.java new file mode 100644 index 0000000..8623064 --- /dev/null +++ b/src/main/java/jota/utils/Multisig.java @@ -0,0 +1,221 @@ +package jota.utils; + +import jota.model.Bundle; +import jota.pow.ICurl; +import jota.pow.JCurl; + +import java.util.Arrays; + +/** + * Created by pinpong on 02.02.17. + */ +public class Multisig { + + private ICurl curl; + private Signing signingInstance; + + public Multisig(ICurl customCurl) { + this.curl = customCurl; + this.signingInstance = new Signing(curl); + } + + public Multisig() { + this.curl = new JCurl(); + } + + /** + * Gets the digest value of a seed + * + * @param {string} seed + * @param {int} index + * @param {int} security + * @method getDigest + * @returns {string} digest trytes + **/ + private String getDigest(String seed, int index, int security) { + + int[] key = signingInstance.key(Converter.trits(seed), index, security); + return Converter.trytes(signingInstance.digests(key)); + } + + /** + * Initiates the generation of a new multisig address or adds the key digest to an existing one + * + * @param digestTrytes + * @param curlStateTrytes + * @method addAddressDigest + * @returns {String} + **/ + private String addAddressDigest(String digestTrytes, String curlStateTrytes, ICurl customCurl) { + + int[] digest = Converter.trits(digestTrytes); + + // If curlStateTrytes is provided, convert into trits + // else use empty state and initiate the creation of a new address + + int[] curlState = curlStateTrytes.isEmpty() ? Converter.trits(curlStateTrytes) : new int[243]; + + + ICurl curl = customCurl == null ? new JCurl() : customCurl; + + + // initialize Curl with the provided state + curl.setState(curlState); + // absorb the key digest + curl.absorb(digest); + + return Converter.trytes(curl.getState()); + } + + /** + * Gets the key value of a seed + * + * @param {string} seed + * @param {int} index + * @method getKey + * @returns {string} digest trytes + **/ + + private String getKey(String seed, int index, int security) { + + return Converter.trytes(signingInstance.key(Converter.trits(seed), index, security)); + } + + /** + * Generates a new address + * + * @param {string} curlStateTrytes + * @param {string} + * @method finalizeAddress + * @returns {String} address + **/ + private String finalizeAddress(String curlStateTrytes, ICurl customCurl) { + + int[] curlState = Converter.trits(curlStateTrytes); + + // initialize Curl with the provided state + curl.setState(curlState); + + int[] addressTrits = new int[243]; + curl.squeeze(addressTrits); + + // Convert trits into trytes and return the address + return Converter.trytes(addressTrits); + } + + /** + * Validates a generated multisig address + * + * @param multisigAddress + * @param digests + * @method validateAddress + * @returns {bool} + **/ + private boolean validateAddress(String multisigAddress, int[][] digests, ICurl customCurl) { + + // initialize Curl with the provided state + curl.reset(); + + + for (int[] keyDigest : digests) { + curl.absorb(keyDigest); + } + + int[] addressTrits = new int[243]; + curl.squeeze(addressTrits); + + // Convert trits into trytes and return the address + return Converter.trytes(addressTrits).equals(multisigAddress); + } + + /** + * Adds the cosigner signatures to the corresponding bundle transaction + * + * @param {array} bundleToSign + * @param {int} cosignerIndex + * @param {string} inputAddress + * @param {string} key + * @param {function} callback + * @method addSignature + * @returns {array} trytes Returns bundle trytes + **/ + private void addSignature(Bundle[] bundleToSign, String inputAddress, String keyTrytes) { + + + // Get the security used for the private key + // 1 security level = 2187 trytes + int security = (keyTrytes.length() / 2187); + + // convert private key trytes into trits + int[] key = Converter.trits(keyTrytes); + + + // First get the total number of already signed transactions + // use that for the bundle hash calculation as well as knowing + // where to add the signature + int numSignedTxs = 0; + + + for (Bundle bundle : bundleToSign) + + for (int i = 0; i < bundle.getTransactions().size(); i++) { + + if (!bundle.getTransactions().get(i).getAddress().equals(inputAddress)) { + + // If transaction is already signed, increase counter + if (!InputValidator.isNinesTrytes(bundle.getTransactions().get(i).getSignatureFragments(), bundle.getTransactions().get(i).getSignatureFragments().length())) { + + numSignedTxs++; + } + // Else sign the transactions + else { + + String bundleHash = bundle.getTransactions().get(i).getBundle(); + + // First 6561 trits for the firstFragment + int[] firstFragment = Arrays.copyOfRange(key, 0, 6561); + + // Get the normalized bundle hash + int[][] normalizedBundleFragments = new int[3][27]; + int[] normalizedBundleHash = bundle.normalizedBundle(bundleHash); + + + // Split hash into 3 fragments + for (int k = 0; k < 3; k++) { + normalizedBundleFragments[k] = Arrays.copyOfRange(normalizedBundleHash, (k * 27), (k + 1) * 27); + } + + + // First bundle fragment uses 27 trytes + int[] firstBundleFragment = normalizedBundleFragments[numSignedTxs % 3]; + + // Calculate the new signatureFragment with the first bundle fragment + int[] firstSignedFragment = signingInstance.signatureFragment(firstBundleFragment, firstFragment); + + // Convert signature to trytes and assign the new signatureFragment + bundle.getTransactions().get(i).setSignatureFragments(Converter.trytes(firstSignedFragment)); + + for (int j = 1; j < security; j++) { + + // Next 6561 trits for the firstFragment + int[] nextFragment = Arrays.copyOfRange(key, 6561 * j, (j + 1) * 6561); + + // Use the next 27 trytes + int[] nextBundleFragment = normalizedBundleFragments[(numSignedTxs + j) % 3]; + + // Calculate the new signatureFragment with the first bundle fragment + int[] nextSignedFragment = signingInstance.signatureFragment(nextBundleFragment, nextFragment); + + // Convert signature to trytes and add new bundle entry at i + j position + // Assign the signature fragment + bundle.getTransactions().get(i + j).setSignatureFragments(Converter.trytes(nextSignedFragment)); + } + + break; + } + } + } + + return; + } +} \ No newline at end of file