Multi-sig has been a hot topic in the blockchain community recently, with the activation of Ethereum EIP 4337 and the increasing popularity of DeFi applications. However, multi-sig transactions have been available on the Flow blockchain for a few years already, and people have been implementing it in various different languages.
This time however we will showcase how to do it with Golang, Google KMS, React, and flow-go-sdk. We’ll also provide a Github repository with code examples to help you get started. By the end of this article, you’ll have a better understanding of how multi-sig transactions work on Flow and how to build your own multi-sign service.
All of the code used can be found in the following github repository:
Prerequisites
Before we dive into the details of how to create a multi-sign service on the Flow blockchain, you’ll need to have a few things set up first. Here’s what you’ll need:
- Go: You’ll need to have Go installed on your computer in order to run the code examples in this tutorial. You can download Go from the official website: https://golang.org/dl/.
- Node: You’ll also need to have Node.js installed on your computer to run the React frontend of the multi-sign service. You can download Node.js from the official website: https://nodejs.org/en/download/.
- GCP KMS: In order to securely store the private keys used for signing transactions, we’ll be using Google Cloud KMS. You’ll need to have a GCP account set up, have KMS. You will also need to create an admin key and account, if you don’t know how to do that, you can check the GCP KMS Setup section in my previous blog post: https://medium.com/@lu_ka_ra_ch_ki/how-to-authorize-flow-blockchain-transactions-with-google-cloud-key-management-service-3289f8fdf35e. If you’re completely new to Google Cloud, you can check out the official documentation to get started: https://cloud.google.com/docs/.
- Knowledge of Go and React: This tutorial assumes that you have some knowledge of both Go and React. If you’re new to either language, you may want to check out some introductory tutorials before diving into this tutorial.
Multi-Sign on Flow
Multi-party signature transactions, also known as multisig transactions, are a type of transaction that require multiple parties to sign off on it before it can be executed. We can achieve various different things with this feature, such as ownership transfer, access to both account’s storage and private functions, proposer key mitigation etc. When the transaction is initiated, each party involved in the transaction must sign off on it using their private key. Once all parties have signed off on the transaction, it is broadcast to the network and executed.
If you’re curious to find out more about how flow transactions and specifically transactions with multiple signers work I suggest checking the official documentation:
Installation
If you’ve completed the steps described in the prerequisites section you are ready to clone and install the repositories dependencies:
Clone the flow-go-multisign
repository from GitHub to your local machine:
git clone https://github.com/lukaracki/flow-go-multisign
Install the required dependencies for the backend service:
cd flow-go-multisign/backend
go mod downloa
Install the required dependencies for the frontend application:
cd ../frontend npm install
Set up the necessary environment variables for the backend and frontend:
1. — Backend
- Create a
.env
file in the root directory of the project and add the following environment variables: - Make sure to replace the
ADMIN_ADDRESS
andADMIN_KEY_INDEX
with the correct values for your Flow account admin address and key index, and replaceGCP_KMS_RESOURCE_NAME
with your own GCP KMS resource name.
# FLOW
ADMIN_ADDRESS=0x01cf0e2f2f715450 - Your flow account admin address
ADMIN_KEY_INDEX=0 - Your flow account admin key index
# GCP KMS
GCP_KMS_RESOURCE_NAME='projects/your-project-id/locations/global/keyRings/flow/cryptoKeys/flow-minter-key/cryptoKeyVersions/1' - Your GCP KMS resource nameFrontend
2. — Frontend
- Create a
.env.local
file in the root directory of the project and add the following environment variables: - Make sure to replace the
REACT_ADMIN_ADDRESS
andREACT_ADMIN_KEY_INDEX
with the correct values for your Flow account admin address and key index.
# FLOW
REACT_ADMIN_ADDRESS=0x01cf0e2f2f715450 - Your flow account admin address
REACT_ADMIN_KEY_INDEX=0 - Your flow account admin key index
Starting the Service
To start the frontend and backend, follow these steps:
Open a new terminal window and navigate to the project directory:
cd flow-go-multisign
Start the backend service:
cd backend
go run cmd/server/main.go
This will start the backend service, which will listen for incoming requests on port 8080
.
Open another terminal window and navigate to the project directory:
cd flow-go-multisign
Start the frontend service:
cd frontend
npm run start
This will start the frontend service, which will be accessible at http://localhost:3000
.
Once both services are up and running, you can access the frontend in your web browser at http://localhost:3000
. From there, you can start playing around with it and see that it actually works.
How it Works
Great, we’ve setup everything and started the service, it works. But how?
Let’s start from the frontend:
In the src/App.js
file we have a method which calls a transactions with multiple signers:
const multiSign = async () => {
setTransaction('');
const transactionId = await fcl
.send([
fcl.transaction`
transaction() {
prepare(frontendUser: AuthAccount, backendAdmin: AuthAccount) {
}
}
`,
fcl.payer(serverAuthorization),
fcl.proposer(fcl.authz),
fcl.authorizations([fcl.authz, serverAuthorization]),
fcl.limit(9999),
])
.then(fcl.decode);
setTransaction(transactionId);
};
We can see that we specify that the payer and second authorizer from the authorizations array is serverAuthorization
. serverAuthorization is defined in the serverSigner.js
file:
const API = 'http://localhost:8080';
const addr = process.env.REACT_APP_ADMIN_ADDRESS;
const keyId = process.env.REACT_APP_ADMIN_KEY_INDEX;
const signingFunction = async (signable) => {
const response = await fetch(`${API}/cosign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payload: signable }),
});
const signature = Buffer.from(await response.json(), 'base64').toString('hex');
return {
addr: fcl.withPrefix(addr),
keyId,
signature,
};
};
export const serverAuthorization = async (account) => {
return {
...account,
tempId: `${addr}-${keyId}`,
addr: fcl.sansPrefix(addr),
keyId: Number(keyId),
signingFunction,
};
};
serverAuthorization
creates a signature object using the signingFunction
which sends a POST request to our Go service, sending the signable object inside the body.
In the following step our backend receives the request:
Inside the handler.go
file we have defined a handler for the POST /cosign request:
type Req struct {
Payload interfaces.Signable
}
// AuthHandler is the handler for the cosign endpoint
// endpoint: POST /cosign
func AuthHandler(c *gin.Context) {
var req Req
if err := c.BindJSON(&req); err != nil {
c.Error(err)
c.AbortWithStatus(http.StatusBadRequest)
return
}
response, err := ProcessSignable(c, &req.Payload)
if err != nil {
c.Error(err)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return
}
c.JSON(http.StatusOK, response)
}
Inside the AuthHandler
we take the payload field from the request object and forward it to the ProcessSignable
method.
// ProcessSignable is a function that processes the signable object, first it decodes the hex string
// and then it decodes the transaction. It then checks the transaction and if it is valid it will
// sign the transaction and return the signature.
func ProcessSignable(c *gin.Context, signable *interfaces.Signable) ([]byte, error) {
decoded, err := hex.DecodeString(signable.Message[64:])
if err != nil {
return nil, fmt.Errorf("error decoding hex string: %w", err)
}
transaction, err := flow.DecodeTransaction(decoded)
if err != nil {
return nil, fmt.Errorf("error decoding transaction: %w", err)
}
if checkTransaction(transaction) {
return sign.SignVoucher(transaction)
} else {
err = fmt.Errorf("invalid request: you are not authorized to request this signature")
return nil, err
}
}
ProcessSignable
takes the Message part of the signable object, which is a hex encoded string. We decode it and construct a transaction object from the flow go SDK by using flow.DecodeTransaction(decoded)
. After we construct the transaction object, we first check it with the checkTransaction
function, if we’re satisfied with the check, we forward the transaction object to the SingVoucher
function.
checkTransaction
is used to check different fields of the transaction that we want to check. You can check any field you want here, in this example implementation we’ve checked the transaction arguments and transaction script.
// checkTransaction is a function that checks if all of the relevant transaction fields are valid
// and returns true if it is valid and false if it is not valid
func checkTransaction(transaction *flow.Transaction) bool {
// Check if the transaction arguments lenght is 0
return checkArguments(transaction.Arguments) && checkScript(transaction.Script)
}
func checkArguments(arguments [][]byte) bool {
// If the transaction arguments lenght is 0, return true
return len(arguments) == 0
}
func checkScript(clientScript []byte) bool {
pattern := regexp.MustCompile(`\s`)
// Replace all the whitespace in the client script and compare it to the server script
return pattern.ReplaceAllString(string(clientScript), "") == pattern.ReplaceAllString(serverScript, "")
}
// Constant variable which holds the server script
const serverScript = `
transaction() {
prepare(frontendUser: AuthAccount, backendAdmin: AuthAccount) {
}
}`
The final step of the process from the service side is the signing of the transaction. This is executed in the SignVoucher
function. It uses the GCP KMS key we’ve specified inside our .env file to create a KMS Client
and a Signer
. Finally, it signs and returns the signed transaction envelope.
package sign
import (
"context"
"github.com/lukaracki/flow-go-multisign/backend/pkg/config"
"github.com/onflow/flow-go-sdk"
"github.com/onflow/flow-go-sdk/crypto/cloudkms"
)
// SignVoucher is a function that signs the transaction and returns the signature
func SignVoucher(transaction *flow.Transaction) ([]byte, error) {
ctx := context.Background()
// Create a key from the resource ID
accountKMSKey, err := cloudkms.KeyFromResourceID(config.GCPKmsResourceName)
if err != nil {
return nil, err
}
// Create a client
kmsClient, err := cloudkms.NewClient(ctx)
if err != nil {
return nil, err
}
// Create a signer
signer, err := kmsClient.SignerForKey(
ctx,
accountKMSKey,
)
if err != nil {
return nil, err
}
// Sign the envelope
err = transaction.SignEnvelope(flow.HexToAddress(config.AdminAddress), config.AdminKeyIndex, signer)
if err != nil {
return nil, err
}
// Return the encoded message
return transaction.EnvelopeSignatures[len(transaction.EnvelopeSignatures)-1].Signature, nil
}
If everything went smoothly, the frontend will receive the signed object inside the POST requests response, and it will construct and submit the transaction to the blockchain.
Specifically for this demo app, you will see a transaction’s hash pop up on the screen and you can click it to inspect the transaction via Flow View Source.
Conclusion
Concluding this blog post, I hope you’ve figured out how to create a multi-sign service for Flow with Go (yes, it rhymes).
Special shoutout to Jacob Tucker, who created a multi-sign tutorial for Node.js, I’ve forked his frontend for use in this repository:
https://github.com/jacob-tucker/multi-sign
Thanks for reading!