Skip to Content
Getting StartedMulti-sig Transfer using Native Script

Multi-sig Transfer using Native Script

Overview

In this guide, we will go through an example of multi-sig transaction using native script. The example will demonstrate the steps required to create a multi-sig script and claim fund from the script address.

Let’s go through some theory first. Click here to directly jump to the example section.

What’s Native Script or Multi-Sig Script ?

A native script is a set of rules that defines how you can spend a UTxO. Native scripts are used to make script addresses where the authorisation condition for a transaction to use that address is that the transaction has signatures from multiple cryptographic keys. Examples include M of N schemes, where a transaction can be authorized if at least M distinct keys, from a set of N keys, sign the transaction.

Some key points about native scripts in Cardano

  • A native script can be encoded as a json text file
  • It uses a simple language for expressing witness requirements for spending an output, minting or burning transaction
  • Keys are identified in the script by hash of public key
  • It also provides support for time-locking, so the script is valid for a specific time range

The simplest native script requires just one key

Example:

{ "type": "sig", "keyHash": "e09d36c79dec9bd1b3d9e152247701cd0bb860b5ebfd1de8abb6735a" }

Types

There are six supported constructors in a multi-sig script. The following section briefly explains about each constructor and corresponding type in json file.

  1. RequireSignature: has the hash of a verification key. This expression evaluates to true if the transaction is signed by a particular key, identified by its verification key hash. Type: “sig”

  2. RequireAllOf: has a list of multisig sub-expressions. This expression evaluates to true if (and only if) all the sub-expressions evaluate to true. Type: “all”

  3. RequireAnyOf: has a list of multisig sub-expressions. This expression evaluates to true if (and only if) any the sub-expressions evaluate to true. Type: “any”

  4. RequireMOf: has a number M and a list of multisig sub-expressions. This expression evaluates to true if (and only if) at least M of the sub-expressions evaluate to true. Type: “atLeast”

  5. RequireTimeBefore: has a slot number X. This condition guarantees that the actual slot number in which the transaction is included is (strictly) less than slot number X. Type: “before”

  6. RequireTimeAfter: has a slot number X. This condition guarantees that the actual slot number in which the transaction is included is greater than or equal to slot number X. Type: “after”

You can find more info about native script here 

Cardano Client Lib provides apis and helpers to create and manage native scripts in Java.

Simple Transfer from a multi-sig script address

In this example we are going to simulate a joint account use case using a multi-sig script. So a muti-sig account between three parties which specifies that at least any two members need to sign to spend fund from the account.

In next few sections, we are going to

  • Create a multi-sig script with three key hashes (2 regular accounts, 1 payment key pair(skey & vkey))
  • Create script address for the multi-sig script
  • Top up multi-sig address with some fund
  • Claim fund from the multi-sig address and distribute to two regular accounts

Accounts Setup

We will use 2 regular accounts and 1 payment key pair (Ed25519 keys) for our example.

The payment key pair is not mandatory, but it is included to demonstrate the transaction signing capability using SecretKey.

To spend fund from our multi-sig script address, we need to sign the transaction with at least 2 keys. Any of the below should work for our use case

  • Sign with 2 regular accounts
  • Sign with 1 regular account and payment secret key
  • Sign with 2 regular account and payment secret key

To create two new regular accounts, check here.

  1. Create account-1 and account-2 from existing mnemonics
String account1Mnemonic = "<24 words mnemonic>"; Account account1 = Account.createFromMnemonic(Networks.testnet(), account1Mnemonic); String account2Mnemonic = "<24 words mnemonic>"; Account account2 = Account.createFromMnemonic(Networks.testnet(), account2Mnemonic);
  1. Create a new payment key pair (Ed25519 secret key / verification key)
Keys keys = KeyGenUtil.generateKey(); VerificationKey verificationKey = keys.getVkey(); SecretKey secretKey = keys.getSkey(); String paymentSigningKeyCborHex = secretKey.getCborHex();

Alternatively, you can create a SecretKey and VerificationKey from an existing payment secret key.

String paymentSigningKeyCborHex = "58205d9ccc4202bde1785708c10f8b13231d5a57078c326d0e0ff172191f975a983e"; SecretKey secretKey = new SecretKey(paymentSigningKeyCborHex); VerificationKey verificationKey = KeyGenUtil.getPublicKeyFromPrivateKey(secretKey);

Create Multi-sig script with 2 regular accounts and 1 payment key pair

We are going to create a multi-sig script with “RequireMOf” constructor or “atLeast” type. For our example, we first need to create verification keys for account1 and account2 using their public key. These verification keys, including the one from payment key pair, are then used to create corresponding ScriptPubKey (sig type native script) instances.

Finally, all three ScriptPubKey instances can be composed to build a ScriptAtLeast instance.

  1. Create VerificationKey instances using public keys of account1 and account2. Use verification keys to create ScriptPubKey instances.
VerificationKey account1Vk = VerificationKey.create(account1.publicKeyBytes()); VerificationKey account2Vk = VerificationKey.create(account2.publicKeyBytes()); ScriptPubkey scriptPubkey1 = ScriptPubkey.create(account1Vk); ScriptPubkey scriptPubkey2 = ScriptPubkey.create(account2Vk);
  1. Use VerificationKey of payment key pair (3rd key) to create the third ScriptPubKey
ScriptPubkey scriptPubkey3 = ScriptPubkey.create(verificationKey);
  1. Now we can use the above ScriptPubKey instances to create a multi-sig “atLeast” instance
ScriptAtLeast scriptAtLeast = new ScriptAtLeast(2) .addScript(scriptPubkey1) .addScript(scriptPubkey2) .addScript(scriptPubkey3);

If you convert scriptAtLeast object to json, you will see something similar

{ "type" : "atLeast", "required" : 2, "scripts" : [ { "type" : "sig", "keyHash" : "0d30c6d716fd6c48ab546f0b66fd5faaa3a2f0ccecf0a72ea8c04a30" }, { "type" : "sig", "keyHash" : "1737dd414cf68676312db8af317fc89167292302b97d65a2d1def5a2" }, { "type" : "sig", "keyHash" : "cdba7678210358a58160183551f23589fb68ca0f71cd74ce776257fe" } ] }

Create Script Address

Now let’s use AddressProvider to generate a script address from our muti-sig native script.

String scriptAddress = AddressProvider.getEntAddress(scriptAtLeast, Networks.testnet()).toBech32();

Topup script address with test Ada

Based on your selected network (preprod / preview), get some test Ada from the below faucet. You need to provide scriptAddress generated in the previous section to get some test Ada.

https://docs.cardano.org/cardano-testnet/tools/faucet 

Create a Backend Service

Please check here to create a BackendService instance.

Claim fund from script address - Using QuickTx API

Now we are ready to claim fund from the script address using the QuickTx API.

Create QuickTxBuilder

QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService);

Define the Transaction

Use the Tx class to define the transaction. Specify the outputs, attach the native script, and set the script address as the sender.

With QuickTx, you don’t need to manually add the script to the witness set or specify the number of signers for fee calculation — attachNativeScript handles the witness and QuickTx handles balancing automatically.

String address1 = account1.baseAddress(); String address2 = account2.baseAddress(); Tx tx = new Tx() .payToAddress(address1, Amount.ada(25)) .payToAddress(address2, Amount.ada(25)) .attachNativeScript(scriptAtLeast) .from(scriptAddress);

Build, Sign, and Submit

Sign the transaction with at least 2 keys as required by the multi-sig script. In this example, we sign with account1 and secretKey (the third key).

Result<String> result = quickTxBuilder.compose(tx) .withSigner(SignerProviders.signerFrom(account1)) .withSigner(SignerProviders.signerFrom(secretKey)) .completeAndWait(System.out::println);

Alternatively, you can sign with account1 and account2, or with all three keys.

If successful, result.isSuccessful() will return true and result.getValue() will contain the transaction hash.

Now copy the transaction id from the output and then go to a Cardano explorer, Cardanoscan  or Cardano Explorer , to check the transaction details. You may need to wait for few secs to a min depending on the blockchain load.

Full Source Code

import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.AddressProvider; import com.bloxbean.cardano.client.api.model.Amount; import com.bloxbean.cardano.client.api.model.Result; import com.bloxbean.cardano.client.backend.api.BackendService; import com.bloxbean.cardano.client.backend.blockfrost.common.Constants; import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.crypto.KeyGenUtil; import com.bloxbean.cardano.client.crypto.SecretKey; import com.bloxbean.cardano.client.crypto.VerificationKey; import com.bloxbean.cardano.client.function.helper.SignerProviders; import com.bloxbean.cardano.client.quicktx.QuickTxBuilder; import com.bloxbean.cardano.client.quicktx.Tx; import com.bloxbean.cardano.client.transaction.spec.script.ScriptAtLeast; import com.bloxbean.cardano.client.transaction.spec.script.ScriptPubkey; public class MultiSigTransfer { public void transfer() throws Exception { //First account String account1Mnemonic = "turkey elder dad veteran they lumber feature garment race answer file erase riot resist sting process law deputy turtle foil legal calm exist civil"; Account account1 = Account.createFromMnemonic(Networks.testnet(), account1Mnemonic); //Second account String account2Mnemonic = "report crowd trophy rough twin column access include evolve awkward world random bounce pave select rocket vote junk farm group main boat tissue mad"; Account account2 = Account.createFromMnemonic(Networks.testnet(), account2Mnemonic); //Third account payment key String paymentSigningKeyCborHex = "58205d9ccc4202bde1785708c10f8b13231d5a57078c326d0e0ff172191f975a983e"; SecretKey secretKey = new SecretKey(paymentSigningKeyCborHex); VerificationKey verificationKey = KeyGenUtil.getPublicKeyFromPrivateKey(secretKey); //Derive verification key for account1 and account2 VerificationKey account1Vk = VerificationKey.create(account1.publicKeyBytes()); VerificationKey account2Vk = VerificationKey.create(account2.publicKeyBytes()); //Create native script with type=sig for each verification key ScriptPubkey scriptPubkey1 = ScriptPubkey.create(account1Vk); ScriptPubkey scriptPubkey2 = ScriptPubkey.create(account2Vk); ScriptPubkey scriptPubkey3 = ScriptPubkey.create(verificationKey); //Create multi-sig script with type "atLeast" ScriptAtLeast scriptAtLeast = new ScriptAtLeast(2) .addScript(scriptPubkey1) .addScript(scriptPubkey2) .addScript(scriptPubkey3); //addr_test1wr6fvn0y3rumu30ch3lrggss4vmgsr65cxly2t6dulvwamq3y98et String scriptAddress = AddressProvider.getEntAddress(scriptAtLeast, Networks.testnet()).toBech32(); // For Blockfrost String bf_projectId = "preprod..."; BackendService backendService = new BFBackendService(Constants.BLOCKFROST_PREPROD_URL, bf_projectId); // For Koios //BackendService backendService = new KoiosBackendService(Constants.KOIOS_PREVIEW_URL); // Create QuickTxBuilder QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); // Define transaction String address1 = account1.baseAddress(); String address2 = account2.baseAddress(); Tx tx = new Tx() .payToAddress(address1, Amount.ada(25)) .payToAddress(address2, Amount.ada(25)) .attachNativeScript(scriptAtLeast) .from(scriptAddress); // Build, sign, submit and wait for confirmation Result<String> result = quickTxBuilder.compose(tx) .withSigner(SignerProviders.signerFrom(account1)) .withSigner(SignerProviders.signerFrom(secretKey)) .completeAndWait(System.out::println); System.out.println(result); } public static void main(String[] args) throws Exception { new MultiSigTransfer().transfer(); } }

What’s next ?

Update the multi-sig script to support time-lock. For example, evaluate to true if minimum 2 signatures and slot number is after X.

Hint: Use RequireTimeAfter with RequireMOf to create multi-sig script.

Looking for the Composable Functions version? Check the Multi-sig Transfer - Composable Functions tutorial.

Last updated on