mirror of
https://github.com/gosticks/iota.lib.java.git
synced 2026-10-03 21:16:59 +00:00
multisigs [wip]
This commit is contained in:
@@ -225,7 +225,7 @@ public class IotaAPI extends IotaAPICore {
|
||||
* @param {int} minWeightMagnitude
|
||||
* @return
|
||||
*/
|
||||
public List<Transaction> sendTrytes(final String[] trytes, final int depth, final int minWeightMagnitude) {
|
||||
public List<Transaction> 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<String> 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<Transfer> transfers, Input[] inputs, String address) throws NotEnoughBalanceException, InvalidSecurityLevelException {
|
||||
public SendTransferResponse sendTransfer(String seed, int security, int depth, int minWeightMagnitude, final List<Transfer> 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<String> inputAddress, String remainderAddress, final List<Transfer> 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<String> 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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user