Adding cosignatures to aggregate complete trasactions

This guide will show you how to add cosignatures to an aggregate complete transaction without using the partial cache.

This can be useful for when you are keeping your private keys in an offline device (cold wallet) for security reasons. By signing adding the cosignatures offline, the cosignatories will be able to execute transactions from your cold wallet while keeping their private keys completely safe. Furthermore, it also allows users to avoid unnecessarily locking their funds in aggregate bonded transactions.

Use case

../../_images/aggregate-escrow-11.png

Multi-Asset Escrowed Transactions

We will set up an aggregate complete transaction where Alice (TDPXWF2H5G7U2NKZRJD47QR4KZPRULPAMQ4O54IK) will send 100 symbol.xym to Bob (TCHS3AOXFGWGTN2QUUHDCXJ4SBYLIQIPNUPHHA2N) in return for 1 collectible mosaic.

Prerequisites

  • Have 2 accounts account.
  • Load the accounts with enough symbol.xym to pay for transaction fees.
  • One of the accounts must own a mosaic other than the symbol.xym.
  • Both your offline and online workstations are set up for Symbol-CLI and Symbol-SDK.

Step 1: Construct the aggregate complete transaction

  1. Open up a text editor. Then, construct the Aggregate Complete Transaction using Alice’s account.
const networkType = NetworkType.TEST_NET;

// replace with alice private key
const alicePrivatekey = '';
const aliceAccount =  Account.createFromPrivateKey(alicePrivatekey, networkType);

// replace with bob public key
const bobPublicKey = '';
const bobPublicAccount = PublicAccount.createFromPublicKey(bobPublicKey, networkType);

const aliceTransferTransaction = TransferTransaction.create(
    Deadline.create(),
    bobPublicAccount.address,
    [NetworkCurrencyPublic.createRelative(1000)],
    PlainMessage.create('payout'),
    networkType,
);

const bobTransferTransaction = TransferTransaction.create(
    Deadline.create(),
    aliceAccount.address,
    [new Mosaic(new NamespaceId('collectible'), UInt64.fromUint(1))],
    PlainMessage.create('payout'),
    networkType,
);

const aggregateTransaction = AggregateTransaction.createComplete(
    Deadline.create(),
    [aliceTransferTransaction.toAggregate(aliceAccount.publicAccount), bobTransferTransaction.toAggregate(bobPublicAccount)],
    networkType,
    [],
    UInt64.fromUint(2000000),
);
const networkType = symbol_sdk_1.NetworkType.TEST_NET;
// replace with alice private key
const alicePrivatekey = '';
const aliceAccount = symbol_sdk_1.Account.createFromPrivateKey(alicePrivatekey, networkType);
// replace with bob public key
const bobPublicKey = '';
const bobPublicAccount = symbol_sdk_1.PublicAccount.createFromPublicKey(bobPublicKey, networkType);
const aliceTransferTransaction = symbol_sdk_1.TransferTransaction.create(symbol_sdk_1.Deadline.create(), bobPublicAccount.address, [symbol_sdk_1.NetworkCurrencyPublic.createRelative(1000)], symbol_sdk_1.PlainMessage.create('payout'), networkType);
const bobTransferTransaction = symbol_sdk_1.TransferTransaction.create(symbol_sdk_1.Deadline.create(), aliceAccount.address, [new symbol_sdk_1.Mosaic(new symbol_sdk_1.NamespaceId('collectible'), symbol_sdk_1.UInt64.fromUint(1))], symbol_sdk_1.PlainMessage.create('payout'), networkType);
const aggregateTransaction = symbol_sdk_1.AggregateTransaction.createComplete(symbol_sdk_1.Deadline.create(), [aliceTransferTransaction.toAggregate(aliceAccount.publicAccount), bobTransferTransaction.toAggregate(bobPublicAccount)], networkType, [], symbol_sdk_1.UInt64.fromUint(2000000));
            NetworkType networkType = repositoryFactory.getNetworkType().toFuture().get();
            NetworkCurrency networkCurrency = repositoryFactory.getNetworkCurrency().toFuture()
                .get();

            // replace with alice private key
            String alicePrivatekey = "";
            Account aliceAccount = Account.createFromPrivateKey(alicePrivatekey, networkType);

            // replace with bob public key
            String bobPublicKey = "";
            PublicAccount bobPublicAccount = PublicAccount
                .createFromPublicKey(bobPublicKey, networkType);

            TransferTransaction aliceTransferTransaction = TransferTransactionFactory
                .create(networkType, bobPublicAccount.getAddress(),
                    Collections
                        .singletonList(networkCurrency.createRelative(BigInteger.valueOf(1000))),
                    PlainMessage.create("payout")).build();

            TransferTransaction bobTransferTransaction = TransferTransactionFactory
                .create(networkType, aliceAccount.getAddress(),
                    Collections.singletonList(
                        new Mosaic(new NamespaceId("collectible"), BigInteger.valueOf(1))),
                    PlainMessage.create("payout")).build();

            AggregateTransaction aggregateTransaction = AggregateTransactionFactory
                .createComplete(networkType, Arrays
                    .asList(aliceTransferTransaction.toAggregate(aliceAccount.getPublicAccount()),
                        bobTransferTransaction.toAggregate(bobPublicAccount)))
                .maxFee(BigInteger.valueOf(2000000)).build();

Make sure to place Alice’s private key and Bob’s public key in the appropriate places.

  1. Sign the transaction with Alice’s key.
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const generationHash = '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';

const signedTransactionNotComplete = aliceAccount.sign(aggregateTransaction, generationHash);
console.log(signedTransactionNotComplete.payload);
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const generationHash = '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransactionNotComplete = aliceAccount.sign(aggregateTransaction, generationHash);
console.log(signedTransactionNotComplete.payload);
            String generationHash = repositoryFactory.getGenerationHash().toFuture().get();

            SignedTransaction signedTransactionNotComplete = aliceAccount
                .sign(aggregateTransaction, generationHash);
            System.out.println(signedTransactionNotComplete.getPayload());

3. Save the TypeScript file, then run it on your terminal. Copy the returned payload and send it over to Bob.

Step 2: Add the second cosignature

  1. Bob cosigns the payload obtained from the previous step.
// replace with bob private key
const bobPrivateKey = '';
const bobAccount = Account.createFromPrivateKey(bobPrivateKey, networkType);
const cosignedTransactionBob = CosignatureTransaction
.signTransactionPayload(bobAccount, signedTransactionNotComplete.payload, generationHash);
console.log(cosignedTransactionBob.signature);
console.log(cosignedTransactionBob.parentHash);
// replace with bob private key
const bobPrivateKey = '';
const bobAccount = symbol_sdk_1.Account.createFromPrivateKey(bobPrivateKey, networkType);
const cosignedTransactionBob = symbol_sdk_1.CosignatureTransaction
    .signTransactionPayload(bobAccount, signedTransactionNotComplete.payload, generationHash);
console.log(cosignedTransactionBob.signature);
console.log(cosignedTransactionBob.parentHash);
            // replace with bob private key
            String bobPrivateKey = "";
            Account bobAccount = Account.createFromPrivateKey(bobPrivateKey, networkType);
            CosignatureSignedTransaction cosignedTransactionBob = CosignatureTransaction
                .create(aggregateTransaction)
                .signWith(bobAccount);

            System.out.println(cosignedTransactionBob.getSignature());
            System.out.println(cosignedTransactionBob.getParentHash());

2. Bob runs the code snippet in the terminal and obtains the transaction signature and the parent hash. Finally, he shares the information back with Alice.

Step 3: Announce the Aggregate Complete Transaction

Using Bob’s public key, cosignature transaction hash, and signature, recreate the transaction and announce it to the network as complete.

const cosignatureSignedTransactions = [
    new CosignatureSignedTransaction(
        cosignedTransactionBob.parentHash,
        cosignedTransactionBob.signature,
        cosignedTransactionBob.signerPublicKey),
];
const rectreatedAggregateTransactionFromPayload = TransactionMapping
.createFromPayload(signedTransactionNotComplete.payload) as AggregateTransaction;

const signedTransactionComplete = aliceAccount
    .signTransactionGivenSignatures(rectreatedAggregateTransactionFromPayload, cosignatureSignedTransactions, generationHash);
console.log(signedTransactionComplete.hash);

// replace with node endpoint
const nodeUrl = 'http://api-01.us-east-1.096x.symboldev.network:3000';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();

transactionHttp
    .announce(signedTransactionComplete)
    .subscribe((x) => console.log(x), (err) => console.error(err));
const cosignatureSignedTransactions = [
    new symbol_sdk_1.CosignatureSignedTransaction(cosignedTransactionBob.parentHash, cosignedTransactionBob.signature, cosignedTransactionBob.signerPublicKey),
];
const rectreatedAggregateTransactionFromPayload = symbol_sdk_1.TransactionMapping
    .createFromPayload(signedTransactionNotComplete.payload);
const signedTransactionComplete = aliceAccount
    .signTransactionGivenSignatures(rectreatedAggregateTransactionFromPayload, cosignatureSignedTransactions, generationHash);
console.log(signedTransactionComplete.hash);
// replace with node endpoint
const nodeUrl = 'http://api-01.us-east-1.096x.symboldev.network:3000';
const repositoryFactory = new symbol_sdk_1.RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
transactionHttp
    .announce(signedTransactionComplete)
    .subscribe((x) => console.log(x), (err) => console.error(err));
            BinarySerialization serialization = BinarySerializationImpl.INSTANCE;

            AggregateTransactionFactory rectreatedAggregateTransactionFromPayload = (AggregateTransactionFactory) serialization
                .deserializeToFactory(
                    ConvertUtils.getBytes(signedTransactionNotComplete.getPayload()));

            //Added a new cosignature.
            rectreatedAggregateTransactionFromPayload.addCosignatures(cosignedTransactionBob);

            SignedTransaction signedTransactionComplete = aliceAccount
                .sign(rectreatedAggregateTransactionFromPayload.build(), generationHash);
            System.out.println(signedTransactionComplete.getHash());

            TransactionRepository transactionHttp = repositoryFactory.createTransactionRepository();

            transactionHttp.announce(signedTransactionComplete).toFuture().get();

If successful, Alice will have sent 100 symbol.xym to Bob and received 1 collectible mosaic in return.