Steem Multi-Signature Transaction Guide for Beem/Python

in #utopian-io2 years ago (edited)

Image: Skitterphoto, CC0


This post is a contribution to the Multisignature Transaction Guide request by @timcliff.

Edit: In the meantime, beem/beempy was adjusted to support the full procedure out-of-the-box. For a ready-to-use CLI interface for multi-sig coming with beem, please refer to:
Multisignature Transaction Guide for beempy

What Will I Learn?

  • You will learn how to set-up a multi-signature authority for Steem accounts
  • You will learn how to prepare a transaction to be signed by multiple keys
  • You will learn how to sign transactions with multiple keys and broadcast it to the chain


The user must have a basic understanding of the Steem key and account authority system as well as the corresponding weights and thresholds. The user should be familiar with Python and using Python scripts on the command line. Some knowledge about beem is required to go beyond the provided example.



Setting up a multi-signature authority Steem account

The @steemit account is an example of an account that has multiple key authorities for each role:
Blockchain transactions sent in the name of the @steemit account can be signed by any of the two keys per role (owner/active/posting). While this account has multiple authorities, it is not a multi-signature account since the threshold parameter on each role is set to 1 - this means that a signature with weight 1 is sufficient to authorize the transaction.
An account becomes a multi-signature account as soon as the weight of a single signature is not sufficient to reach the threshold. Here is an example of a multi-sig account where this condition is fulfilled:


This account has an active threshold of 40 while each of the authorized keys can provide a weight of at most 25 or 10. This means that transactions requiring the active authority need to be signed by multiple of the authorized keys.

How to set an account into this state with beem

In order to set owner, active or posting authorities, weights and thresholds, the Account_update function from beembase.operations is needed. The operation can be prepared in the following way:

from beem.account import Account
from beembase import operations
acc = Account("accountname")
op = operations.Account_update(**{
    "account": acc["name"],
    'active': acc["active"],
    'posting': acc["posting"],
    'memo_key': acc['memo_key'],
    "json_metadata": acc['json_metadata'],
    "prefix": acc.steem.prefix})

acc["posting"] and acc["active"] are dictionaries in the style of:

authority = {
  'account_auths': [], 
  'key_auths': [
    ['STMKEY1...', 25], 
    ['STMKEY2...', 10]],
  'address_auths': [], 
  'weight_threshold': 100, 
  'prefix': acc.steem.prefix}

(adjust the keys, the weights and the threshold accordingly). Make sure that the sum of the individual weights reaches or exceeds the threshold, otherwise no combination of multiple signatures will be able to work with the active authority.

This operation can then be submitted with a TransactionBuilder instance:

from beem.transactionbuilder import TransactionBuilder
tb = TransactionBuilder(steem_instance=acc.steem)
tx = tb.broadcast()

Since this is modifying the active authority of the account, this operation has to be signed with the owner key. The same concept also applies to the posting authorities, but changing those only required the corresponding active key.

The result is what is shown in the screenshot above and was carried out with the @test.auth account.

A basic, but ready-to-use script to set arbitrary active and posting authorities is provided here:

This script can be used like this:

python \
  --account test.auth \
  --role active \
  --threshold 40 \
  --key-weight STM7JNWU2b2DBMp9WZRGHKqE7hQA9MKn2URLBCMfU95p29vzAQn8F 25 \
  --key-weight STM5iLWNocQk8UDUVpuGytJkmLiEuK9XYvyMH3yYRdXhW6qGqbWA4 25 \
  --key-weight STM6qpVTxhWBuKUFgUHgymuQrtRwZ16bWmyZVf7ba4QzoYypEXS5X 10 \
  --key-weight STM8Hp2EPPgddvQRGYUFomX5v8ixh1LeoFKk3FCTxMwkSvzieGrDg 10 \
  --key-weight STM75MGQ6WLhSqNoUYsm6oXeGp3R5KPpaKgtoLUHTtcS5vhrWmrtJ 10 \
  --key-weight STM6Ukq4htXhnkKDdR45XgCLJY8a8eJV9ndUmLBT32ktPzYdvqPJy 10 \
  --key-weight STM6AFxvirq6P1XTeiYEwfTCMXivJsg9wmZDBrWxG5fQkvgzRZwHX 10

Supported --role parameters are active and posting. The --key-weight parameter takes a space-separated pair of a pubkey and a weight. There is also an --account-weight parameter taking an account name plus weight. Note that there is an upper limit of 40 authorities (keys and accounts combined) per role.

This is the resulting transaction from this script:

Edit: the same state can be achieved via multiplebeempy commands as suggested by @tcpolymath in the comments, e.g.:

$ beempy allow -a "test.auth" --permission active --weight 25 --threshold 40 STM7JNWU2b2DBMp9WZRGHKqE7hQA9MKn2URLBCMfU95p29vzAQn8F

How to prepare a transaction to be signed by multiple keys?

Transaction that are sent to the blockchain have the following format:

{"expiration": "2019-01-04T21:05:55", "ref_block_num": 6032, "ref_block_prefix": 1020164711, "operations": [["transfer", {"from": "test.auth", "to": "test.auth", "amount": "0.001 STEEM", "memo": ""}]], "extensions": [], "signatures": []}

expiration defines how long the transaction is going to be valid. Typical numbers are 1 minute (beem default), the maximum allowed by the blockchain is 1 hour. Since not all signatures are added instantly, choosing a longer period is reasonable in this case. ref_block_num and ref_block_prefix are values that the blockchain provides on request. The operations parameter contains the desired user operation (in this case transferring 0.001 STEEM to self). extensions is typically empty. signatures is a list of strings that is filled by signing this operation. In case of a "normal" transaction, the final signature filed would contain a lengthy string of characters and numbers. Multiple signatures can be achieved by signing the same operation with multiple keys independently, and list all resulting signatures in the signatures field.

In order to create such a base transaction, we can again use the TransactionBuilder class:

op = operations.Transfer(**{
    'from': args.account,
    'amount': Amount(args.amount, args.asset),
    'memo': args.memo
stm = Steem(expiration=3600)
tb = TransactionBuilder(steem_instance=stm)
print("'%s'" % (json.dumps(tb.json())))

(note the override of the expiration period to 1h = 3600s). The resulting output was the transaction string shown above.

A basic, but ready-to-use script to create an arbitrary transfer transaction is provided here:

This script can be used like this:

python \
  --account test.auth \
    --to test.auth \
    --amount 0.001 \
    --asset STEEM

an optional --memo parameter allows you to set a transfer memo as well.

Edit: also this stage can be done directly via beempy:

$ beempy -e 3600 --no-broadcast --unsigned transfer --account "test.auth" "test.auth" 0.001 STEEM

How to sign this transaction now with multiple keys and broadcast it to the chain?

Signing this transaction means appending an entry to the signatures list of the transaction. This signature can be obtained by loading the above transaction with the TransactionBuilder class, append a private key and use the sign() method. Each time this step is done, another signature is appended to the list:

tb = TransactionBuilder(tx=op)

Important here is that the transaction has to be passed to the TransactionBuilder constructor and the sign method has to be instructed not to reconstruct the transaction but take it as it is. If this parameter is not set, the class will strip off existing signatures and update expiration, ref_block_num and ref_block_prefix to the current values.

Once the required number of signatures is appended to meet or exceed the threshold, the transaction can be broadcasted:

tx = tb.broadcast()

A basic, but ready-to-use script to sign transaction with multiple keys and broadcast it to the chain is provided here:

This script can be used like shown below. The same script is fed multiple times with the output of the previous step each. This ensures, that the same transaction can be signed successively by independent people and broadcasted as soon as the weight threshold is reached:

# first signature: run with the trx created above:
$ python --append-sig --trx '{"expiration": "2019-01-04T22:13:41", "ref_block_num": 7386, "ref_block_prefix": 3706177428, "operations": [["transfer", {"from": "test.auth", "to": "test.auth", "amount": "0.001 STEEM", "memo": ""}]], "extensions": [], "signatures": []}'
Enter private key:
'{"expiration": "2019-01-04T22:13:41", "ref_block_num": 7386, "ref_block_prefix": 3706177428, "operations": [["transfer", {"from": "test.auth", "to": "test.auth", "amount": "0.001 STEEM", "memo": ""}]], "extensions": [], "signatures": ["1f65df7e3fb5f3ad49c433054127d71918c10865d67367efa380edee2e9943540d1e6f97317a1cdfe547f49d89048a44bd2f9a9a7278e51b9fef98dfddaee576da"]}'

# second signature - run the same script with the output of step 1 - note the two sigs in the output:
$ python --append-sig --trx '{"expiration": "2019-01-04T22:13:41", "ref_block_num": 7386, "ref_block_prefix": 3706177428, "operations": [["transfer", {"from": "test.auth", "to": "test.auth", "amount": "0.001 STEEM", "memo": ""}]], "extensions": [], "signatures": ["1f65df7e3fb5f3ad49c433054127d71918c10865d67367efa380edee2e9943540d1e6f97317a1cdfe547f49d89048a44bd2f9a9a7278e51b9fef98dfddaee576da"]}'
Enter private key:
'{"expiration": "2019-01-04T22:13:41", "ref_block_num": 7386, "ref_block_prefix": 3706177428, "operations": [["transfer", {"from": "test.auth", "to": "test.auth", "amount": "0.001 STEEM", "memo": ""}]], "extensions": [], "signatures": ["1f65df7e3fb5f3ad49c433054127d71918c10865d67367efa380edee2e9943540d1e6f97317a1cdfe547f49d89048a44bd2f9a9a7278e51b9fef98dfddaee576da", "205d6e432fa276f0ef7c5da90703811d01b1358348ec5c1de5bd1504b7990d4f42121542d5d555a8fbb974db316d84714f7da2c43a5160bc00548fda6b15c33687"]}'

# third signature: run the same script with the output of step 2, add the --broadcast parameter and note the three sigs in the output:
$ python --append-sig --trx '{"expiration": "2019-01-04T22:13:41", "ref_block_num": 7386, "ref_block_prefix": 3706177428, "operations": [["transfer", {"from": "test.auth", "to": "test.auth", "amount": "0.001 STEEM", "memo": ""}]], "extensions": [], "signatures": ["1f65df7e3fb5f3ad49c433054127d71918c10865d67367efa380edee2e9943540d1e6f97317a1cdfe547f49d89048a44bd2f9a9a7278e51b9fef98dfddaee576da", "205d6e432fa276f0ef7c5da90703811d01b1358348ec5c1de5bd1504b7990d4f42121542d5d555a8fbb974db316d84714f7da2c43a5160bc00548fda6b15c33687"]}' --broadcast
Enter private key:
SUCCESS: broadcasted '{
  "expiration": "2019-01-04T22:13:41",
  "ref_block_num": 7386,
  "ref_block_prefix": 3706177428,
  "operations": [
        "from": "test.auth",
        "to": "test.auth",
        "amount": "0.001 STEEM",
        "memo": ""
  "extensions": [],
  "signatures": [

here's the resulting transaction on steemd:

And an example transferring 0.001 STEEM to my account with 4 signatures as required when using only the keys with weight=10:

(this step is currently not yet possible with beempy, but there is work in progress to fully integrate all steps)

Further thoughts:

  • A big limitation for mass adoption of multi-sig is the fact that the maximum transaction expiration time can only be 1h at most. This can be a at least annoying or even a problem if multiple people that live in different time zones or have a life outside of Steem have to work together to reach the signature threshold.
  • Even with a multi-sig active authority, transfers can still be done with the owner authority (currently requiring only one sig with the example account).
  • However, the exact same concept can be applied to the owner authority as well. This has to be done carefully, since a mistake there could render the account unusable and the funds locked. With access to the recovery partner's active key, it could still be recovered, though.
  • For the signing and broadcasting part, there is currently no automated check if the required number of signatures to meet or exceed the threshold is reached, but this could be implemented by reconstructing the signers of the existing signatures and checking their weight against the required threshold.

Proof of Work Done

All code shown here is available as a GitHub Gist:


Thank you for your contribution.

  • Interesting to see the original guide request by @timcliff, this could have large implications on the Steem blockchain.
  • Overall your coverage of the topic is well detailed.
  • Appreciate you making edits to include alternative approach using beempy, makes the tutorial more elaborate.
  • Good use of screenshots and output results. Gists are quite helpful too! Few additional comments there would have been more helpful.
  • Also loved your additional thoughts on the whole concept overall.

Good luck winning the bounty :)

Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.

Need help? Write a ticket on
Chat with us on Discord.

Thanks for your review, @mcfarhat!

Thank you for your review, @mcfarhat! Keep up the good work!

I really wanted to write pretty much this exact tutorial for a few months, but sadly I couldn't motivate myself to tinker around with that stuff. Sadly school is too demanding this year, and if I technically have free time (like right now for example), I either want to do nothing, or have other more important things to do :(((
But looking at your post, I know that it's better than my post would've been. For sure ;-)
Let's hope I can finally get myself to write that post about me taking away those delegations D:

Thanks for your post, it's neat :P

haha, I know that state, I find myself in that position often enough as well :) The contest was a good "kick" to actually give it a try. Looking forward to your delegation removal post! I noticed your recent PR in the irredeemables list and you had plenty of them in the past - make the post, some rewards for your work would be well deserved ;)

I basically did exactly this but was too slow about it, so I'm pretty sure it works.

The first part can be done much, much more easily with beempy's allow command.

allow -a targetaccount --permission active --weight 10 signingaccount

and then when you get to the last one add --threshold 40 after the weight. (Don't do it before or you won't be able to add new ones without the owner key or building a multisig transaction to do it.)

(Don't do it before or you won't be able to add new ones without the owner key or building a multisig transaction to do it.)

For changing the active authority, this actually doesn't matter since you need the owner key for anyway. You can't chance the active authority with an active key. It doesn't matter if there is an intermediate situation where none of the active keys can reach the active threshold.
edit: You are right, it is possible to change the active auth with active keys - using the owner key in the first place is an easy workaround.
It makes a difference though, if the owner authority is changed. But in that case it's probably better to use a script like mine and set them all at once unless you're OK with waiting one hour after each change.

In fact I haven't been able to do it with the owner key; beempy crashes out with "Unnecessary signature(s) detected."

Thank you so much for participating the Partiko Delegation Plan Round 1! We really appreciate your support! As part of the delegation benefits, we just gave you a 3.00% upvote! Together, let’s change the world!

Even with a multi-sig active authority, transfers can still be done with the owner authority (currently requiring only one sig with the example account).

However, the exact same concept can be applied to the owner authority as well. This has to be done carefully, since a mistake there could render the account unusable and the funds locked.

In looking for an easy way to get my test account usable again, I discovered that a master password reset will automatically change the weight of all that account's native keys to the Threshold weight, presumably including the owner key, though I didn't test for it. I don't know that there's any way to prevent that.

I would presume account recovery behaves similarly.

The account recovery on its own only affects the owner authority, all other authorities are not touched. In the Request_account_recovery operation, you can specify how the new authority should look like, including any account_auths, key_auths, weights and the threshold - see for example here. Changing any lower-priviledged keys is then possible from the new owner authority.

Hi @crokkon!

Your post was upvoted by @steem-ua, new Steem dApp, using UserAuthority for algorithmic post curation!
Your UA account score is currently 5.006 which ranks you at #1086 across all Steem accounts.
Your rank has improved 1 places in the last three days (old rank 1087).

In our last Algorithmic Curation Round, consisting of 207 contributions, your post is ranked at #8.

Evaluation of your UA score:
  • You've built up a nice network.
  • The readers appreciate your great work!
  • Great user engagement! You rock!

Feel free to join our @steem-ua Discord server

This post has been included in today's SOS Daily News - a digest of all you need to know about the State of Steem.

Hey, @crokkon!

Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Get higher incentives and support!
Simply set as a 5% (or higher) payout beneficiary on your contribution post (via SteemPlus or Steeditor).

Want to chat? Join us on Discord

Vote for Utopian Witness!

Coin Marketplace

STEEM 0.90
TRX 0.12
JST 0.112
BTC 50245.65
ETH 3983.82
BNB 611.63
SBD 6.03