github.com/filecoin-project/specs-actors/v4@v4.0.2/actors/builtin/multisig/multisig_actor.go (about)

     1  package multisig
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  
     7  	addr "github.com/filecoin-project/go-address"
     8  	"github.com/filecoin-project/go-state-types/abi"
     9  	"github.com/filecoin-project/go-state-types/big"
    10  	"github.com/filecoin-project/go-state-types/cbor"
    11  	"github.com/filecoin-project/go-state-types/exitcode"
    12  	multisig0 "github.com/filecoin-project/specs-actors/actors/builtin/multisig"
    13  	multisig2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/multisig"
    14  
    15  	"github.com/ipfs/go-cid"
    16  
    17  	"github.com/filecoin-project/specs-actors/v4/actors/builtin"
    18  	"github.com/filecoin-project/specs-actors/v4/actors/runtime"
    19  	"github.com/filecoin-project/specs-actors/v4/actors/util/adt"
    20  )
    21  
    22  type TxnID = multisig0.TxnID
    23  
    24  //type Transaction struct {
    25  //	To     addr.Address
    26  //	Value  abi.TokenAmount
    27  //	Method abi.MethodNum
    28  //	Params []byte
    29  //
    30  //	// This address at index 0 is the transaction proposer, order of this slice must be preserved.
    31  //	Approved []addr.Address
    32  //}
    33  type Transaction = multisig0.Transaction
    34  
    35  // Data for a BLAKE2B-256 to be attached to methods referencing proposals via TXIDs.
    36  // Ensures the existence of a cryptographic reference to the original proposal. Useful
    37  // for offline signers and for protection when reorgs change a multisig TXID.
    38  //
    39  // Requester - The requesting multisig wallet member.
    40  // All other fields - From the "Transaction" struct.
    41  //type ProposalHashData struct {
    42  //	Requester addr.Address
    43  //	To        addr.Address
    44  //	Value     abi.TokenAmount
    45  //	Method    abi.MethodNum
    46  //	Params    []byte
    47  //}
    48  type ProposalHashData = multisig0.ProposalHashData
    49  
    50  type Actor struct{}
    51  
    52  func (a Actor) Exports() []interface{} {
    53  	return []interface{}{
    54  		builtin.MethodConstructor: a.Constructor,
    55  		2:                         a.Propose,
    56  		3:                         a.Approve,
    57  		4:                         a.Cancel,
    58  		5:                         a.AddSigner,
    59  		6:                         a.RemoveSigner,
    60  		7:                         a.SwapSigner,
    61  		8:                         a.ChangeNumApprovalsThreshold,
    62  		9:                         a.LockBalance,
    63  	}
    64  }
    65  
    66  func (a Actor) Code() cid.Cid {
    67  	return builtin.MultisigActorCodeID
    68  }
    69  
    70  func (a Actor) State() cbor.Er {
    71  	return new(State)
    72  }
    73  
    74  var _ runtime.VMActor = Actor{}
    75  
    76  // type ConstructorParams struct {
    77  // 	Signers               []addr.Address
    78  // 	NumApprovalsThreshold uint64
    79  // 	UnlockDuration        abi.ChainEpoch
    80  // 	StartEpoch            abi.ChainEpoch
    81  // }
    82  type ConstructorParams = multisig2.ConstructorParams
    83  
    84  func (a Actor) Constructor(rt runtime.Runtime, params *ConstructorParams) *abi.EmptyValue {
    85  	rt.ValidateImmediateCallerIs(builtin.InitActorAddr)
    86  
    87  	if len(params.Signers) < 1 {
    88  		rt.Abortf(exitcode.ErrIllegalArgument, "must have at least one signer")
    89  	}
    90  
    91  	if len(params.Signers) > SignersMax {
    92  		rt.Abortf(exitcode.ErrIllegalArgument, "cannot add more than %d signers", SignersMax)
    93  	}
    94  
    95  	// resolve signer addresses and do not allow duplicate signers
    96  	resolvedSigners := make([]addr.Address, 0, len(params.Signers))
    97  	deDupSigners := make(map[addr.Address]struct{}, len(params.Signers))
    98  	for _, signer := range params.Signers {
    99  		resolved, err := builtin.ResolveToIDAddr(rt, signer)
   100  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to resolve addr %v to ID addr", signer)
   101  
   102  		if _, ok := deDupSigners[resolved]; ok {
   103  			rt.Abortf(exitcode.ErrIllegalArgument, "duplicate signer not allowed: %s", signer)
   104  		}
   105  
   106  		resolvedSigners = append(resolvedSigners, resolved)
   107  		deDupSigners[resolved] = struct{}{}
   108  	}
   109  
   110  	if params.NumApprovalsThreshold > uint64(len(params.Signers)) {
   111  		rt.Abortf(exitcode.ErrIllegalArgument, "must not require more approvals than signers")
   112  	}
   113  
   114  	if params.NumApprovalsThreshold < 1 {
   115  		rt.Abortf(exitcode.ErrIllegalArgument, "must require at least one approval")
   116  	}
   117  
   118  	if params.UnlockDuration < 0 {
   119  		rt.Abortf(exitcode.ErrIllegalArgument, "negative unlock duration disallowed")
   120  	}
   121  
   122  	pending, err := adt.StoreEmptyMap(adt.AsStore(rt), builtin.DefaultHamtBitwidth)
   123  	if err != nil {
   124  		rt.Abortf(exitcode.ErrIllegalState, "failed to create empty map: %v", err)
   125  	}
   126  
   127  	var st State
   128  	st.Signers = resolvedSigners
   129  	st.NumApprovalsThreshold = params.NumApprovalsThreshold
   130  	st.PendingTxns = pending
   131  	st.InitialBalance = abi.NewTokenAmount(0)
   132  	if params.UnlockDuration != 0 {
   133  		st.SetLocked(params.StartEpoch, params.UnlockDuration, rt.ValueReceived())
   134  	}
   135  
   136  	rt.StateCreate(&st)
   137  	return nil
   138  }
   139  
   140  //type ProposeParams struct {
   141  //	To     addr.Address
   142  //	Value  abi.TokenAmount
   143  //	Method abi.MethodNum
   144  //	Params []byte
   145  //}
   146  type ProposeParams = multisig0.ProposeParams
   147  
   148  //type ProposeReturn struct {
   149  //	// TxnID is the ID of the proposed transaction
   150  //	TxnID TxnID
   151  //	// Applied indicates if the transaction was applied as opposed to proposed but not applied due to lack of approvals
   152  //	Applied bool
   153  //	// Code is the exitcode of the transaction, if Applied is false this field should be ignored.
   154  //	Code exitcode.ExitCode
   155  //	// Ret is the return vale of the transaction, if Applied is false this field should be ignored.
   156  //	Ret []byte
   157  //}
   158  type ProposeReturn = multisig0.ProposeReturn
   159  
   160  func (a Actor) Propose(rt runtime.Runtime, params *ProposeParams) *ProposeReturn {
   161  	rt.ValidateImmediateCallerType(builtin.CallerTypesSignable...)
   162  	proposer := rt.Caller()
   163  
   164  	if params.Value.Sign() < 0 {
   165  		rt.Abortf(exitcode.ErrIllegalArgument, "proposed value must be non-negative, was %v", params.Value)
   166  	}
   167  
   168  	var txnID TxnID
   169  	var st State
   170  	var txn *Transaction
   171  	rt.StateTransaction(&st, func() {
   172  		if !st.IsSigner(proposer) {
   173  			rt.Abortf(exitcode.ErrForbidden, "%s is not a signer", proposer)
   174  		}
   175  
   176  		ptx, err := adt.AsMap(adt.AsStore(rt), st.PendingTxns, builtin.DefaultHamtBitwidth)
   177  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load pending transactions")
   178  
   179  		txnID = st.NextTxnID
   180  		st.NextTxnID += 1
   181  		txn = &Transaction{
   182  			To:       params.To,
   183  			Value:    params.Value,
   184  			Method:   params.Method,
   185  			Params:   params.Params,
   186  			Approved: []addr.Address{},
   187  		}
   188  
   189  		if err := ptx.Put(txnID, txn); err != nil {
   190  			rt.Abortf(exitcode.ErrIllegalState, "failed to put transaction for propose: %v", err)
   191  		}
   192  
   193  		st.PendingTxns, err = ptx.Root()
   194  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush pending transactions")
   195  	})
   196  
   197  	applied, ret, code := a.approveTransaction(rt, txnID, txn)
   198  
   199  	// Note: this transaction ID may not be stable across chain re-orgs.
   200  	// The proposal hash may be provided as a stability check when approving.
   201  	return &ProposeReturn{
   202  		TxnID:   txnID,
   203  		Applied: applied,
   204  		Code:    code,
   205  		Ret:     ret,
   206  	}
   207  }
   208  
   209  //type TxnIDParams struct {
   210  //	ID TxnID
   211  //	// Optional hash of proposal to ensure an operation can only apply to a
   212  //	// specific proposal.
   213  //	ProposalHash []byte
   214  //}
   215  type TxnIDParams = multisig0.TxnIDParams
   216  
   217  //type ApproveReturn struct {
   218  //	// Applied indicates if the transaction was applied as opposed to proposed but not applied due to lack of approvals
   219  //	Applied bool
   220  //	// Code is the exitcode of the transaction, if Applied is false this field should be ignored.
   221  //	Code exitcode.ExitCode
   222  //	// Ret is the return vale of the transaction, if Applied is false this field should be ignored.
   223  //	Ret []byte
   224  //}
   225  type ApproveReturn = multisig0.ApproveReturn
   226  
   227  func (a Actor) Approve(rt runtime.Runtime, params *TxnIDParams) *ApproveReturn {
   228  	rt.ValidateImmediateCallerType(builtin.CallerTypesSignable...)
   229  	approver := rt.Caller()
   230  
   231  	var st State
   232  	var txn *Transaction
   233  	rt.StateTransaction(&st, func() {
   234  		if !st.IsSigner(approver) {
   235  			rt.Abortf(exitcode.ErrForbidden, "%s is not a signer", approver)
   236  		}
   237  
   238  		ptx, err := adt.AsMap(adt.AsStore(rt), st.PendingTxns, builtin.DefaultHamtBitwidth)
   239  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load pending transactions")
   240  
   241  		txn = getTransaction(rt, ptx, params.ID, params.ProposalHash, true)
   242  	})
   243  
   244  	// if the transaction already has enough approvers, execute it without "processing" this approval.
   245  	approved, ret, code := executeTransactionIfApproved(rt, st, params.ID, txn)
   246  	if !approved {
   247  		// if the transaction hasn't already been approved, let's "process" this approval
   248  		// and see if we can execute the transaction
   249  		approved, ret, code = a.approveTransaction(rt, params.ID, txn)
   250  	}
   251  
   252  	return &ApproveReturn{
   253  		Applied: approved,
   254  		Code:    code,
   255  		Ret:     ret,
   256  	}
   257  }
   258  
   259  func (a Actor) Cancel(rt runtime.Runtime, params *TxnIDParams) *abi.EmptyValue {
   260  	rt.ValidateImmediateCallerType(builtin.CallerTypesSignable...)
   261  	callerAddr := rt.Caller()
   262  
   263  	var st State
   264  	rt.StateTransaction(&st, func() {
   265  		callerIsSigner := st.IsSigner(callerAddr)
   266  		if !callerIsSigner {
   267  			rt.Abortf(exitcode.ErrForbidden, "%s is not a signer", callerAddr)
   268  		}
   269  
   270  		ptx, err := adt.AsMap(adt.AsStore(rt), st.PendingTxns, builtin.DefaultHamtBitwidth)
   271  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load pending txns")
   272  
   273  		var txn Transaction
   274  		found, err := ptx.Pop(params.ID, &txn)
   275  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to pop transaction %v for cancel", params.ID)
   276  		if !found {
   277  			rt.Abortf(exitcode.ErrNotFound, "no such transaction %v to cancel", params.ID)
   278  		}
   279  
   280  		proposer := txn.Approved[0]
   281  		if proposer != callerAddr {
   282  			rt.Abortf(exitcode.ErrForbidden, "Cannot cancel another signers transaction")
   283  		}
   284  
   285  		// confirm the hashes match
   286  		calculatedHash, err := ComputeProposalHash(&txn, rt.HashBlake2b)
   287  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to compute proposal hash for %v", params.ID)
   288  		if params.ProposalHash != nil && !bytes.Equal(params.ProposalHash, calculatedHash[:]) {
   289  			rt.Abortf(exitcode.ErrIllegalState, "hash does not match proposal params (ensure requester is an ID address)")
   290  		}
   291  
   292  		st.PendingTxns, err = ptx.Root()
   293  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush pending transactions")
   294  	})
   295  	return nil
   296  }
   297  
   298  //type AddSignerParams struct {
   299  //	Signer   addr.Address
   300  //	Increase bool
   301  //}
   302  type AddSignerParams = multisig0.AddSignerParams
   303  
   304  func (a Actor) AddSigner(rt runtime.Runtime, params *AddSignerParams) *abi.EmptyValue {
   305  	// Can only be called by the multisig wallet itself.
   306  	rt.ValidateImmediateCallerIs(rt.Receiver())
   307  	resolvedNewSigner, err := builtin.ResolveToIDAddr(rt, params.Signer)
   308  	builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to resolve address %v", params.Signer)
   309  
   310  	var st State
   311  	rt.StateTransaction(&st, func() {
   312  		if len(st.Signers) >= SignersMax {
   313  			rt.Abortf(exitcode.ErrForbidden, "cannot add more than %d signers", SignersMax)
   314  		}
   315  
   316  		if st.IsSigner(resolvedNewSigner) {
   317  			rt.Abortf(exitcode.ErrForbidden, "%s is already a signer", resolvedNewSigner)
   318  		}
   319  
   320  		st.Signers = append(st.Signers, resolvedNewSigner)
   321  		if params.Increase {
   322  			st.NumApprovalsThreshold = st.NumApprovalsThreshold + 1
   323  		}
   324  	})
   325  	return nil
   326  }
   327  
   328  //type RemoveSignerParams struct {
   329  //	Signer   addr.Address
   330  //	Decrease bool
   331  //}
   332  type RemoveSignerParams = multisig0.RemoveSignerParams
   333  
   334  func (a Actor) RemoveSigner(rt runtime.Runtime, params *RemoveSignerParams) *abi.EmptyValue {
   335  	// Can only be called by the multisig wallet itself.
   336  	rt.ValidateImmediateCallerIs(rt.Receiver())
   337  	resolvedOldSigner, err := builtin.ResolveToIDAddr(rt, params.Signer)
   338  	builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to resolve address %v", params.Signer)
   339  
   340  	store := adt.AsStore(rt)
   341  	var st State
   342  	rt.StateTransaction(&st, func() {
   343  		if !st.IsSigner(resolvedOldSigner) {
   344  			rt.Abortf(exitcode.ErrForbidden, "%s is not a signer", resolvedOldSigner)
   345  		}
   346  
   347  		if len(st.Signers) == 1 {
   348  			rt.Abortf(exitcode.ErrForbidden, "cannot remove only signer")
   349  		}
   350  
   351  		newSigners := make([]addr.Address, 0, len(st.Signers))
   352  		// signers have already been resolved
   353  		for _, s := range st.Signers {
   354  			if resolvedOldSigner != s {
   355  				newSigners = append(newSigners, s)
   356  			}
   357  		}
   358  
   359  		// if the number of signers is below the threshold after removing the given signer,
   360  		// we should decrease the threshold by 1. This means that decrease should NOT be set to false
   361  		// in such a scenario.
   362  		if !params.Decrease && uint64(len(st.Signers)-1) < st.NumApprovalsThreshold {
   363  			rt.Abortf(exitcode.ErrIllegalArgument, "can't reduce signers to %d below threshold %d with decrease=false", len(st.Signers)-1, st.NumApprovalsThreshold)
   364  		}
   365  
   366  		if params.Decrease {
   367  			if st.NumApprovalsThreshold < 2 {
   368  				rt.Abortf(exitcode.ErrIllegalArgument, "can't decrease approvals from %d to %d", st.NumApprovalsThreshold, st.NumApprovalsThreshold-1)
   369  			}
   370  			st.NumApprovalsThreshold = st.NumApprovalsThreshold - 1
   371  		}
   372  
   373  		err := st.PurgeApprovals(store, resolvedOldSigner)
   374  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to purge approvals of removed signer")
   375  
   376  		st.Signers = newSigners
   377  	})
   378  
   379  	return nil
   380  }
   381  
   382  //type SwapSignerParams struct {
   383  //	From addr.Address
   384  //	To   addr.Address
   385  //}
   386  type SwapSignerParams = multisig0.SwapSignerParams
   387  
   388  func (a Actor) SwapSigner(rt runtime.Runtime, params *SwapSignerParams) *abi.EmptyValue {
   389  	// Can only be called by the multisig wallet itself.
   390  	rt.ValidateImmediateCallerIs(rt.Receiver())
   391  
   392  	fromResolved, err := builtin.ResolveToIDAddr(rt, params.From)
   393  	builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to resolve from address %v", params.From)
   394  
   395  	toResolved, err := builtin.ResolveToIDAddr(rt, params.To)
   396  	builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to resolve to address %v", params.To)
   397  
   398  	store := adt.AsStore(rt)
   399  	var st State
   400  	rt.StateTransaction(&st, func() {
   401  		if !st.IsSigner(fromResolved) {
   402  			rt.Abortf(exitcode.ErrForbidden, "from addr %s is not a signer", fromResolved)
   403  		}
   404  
   405  		if st.IsSigner(toResolved) {
   406  			rt.Abortf(exitcode.ErrIllegalArgument, "%s already a signer", toResolved)
   407  		}
   408  
   409  		newSigners := make([]addr.Address, 0, len(st.Signers))
   410  		for _, s := range st.Signers {
   411  			if s != fromResolved {
   412  				newSigners = append(newSigners, s)
   413  			}
   414  		}
   415  		newSigners = append(newSigners, toResolved)
   416  		st.Signers = newSigners
   417  
   418  		err := st.PurgeApprovals(store, fromResolved)
   419  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to purge approvals of removed signer")
   420  	})
   421  
   422  	return nil
   423  }
   424  
   425  //type ChangeNumApprovalsThresholdParams struct {
   426  //	NewThreshold uint64
   427  //}
   428  type ChangeNumApprovalsThresholdParams = multisig0.ChangeNumApprovalsThresholdParams
   429  
   430  func (a Actor) ChangeNumApprovalsThreshold(rt runtime.Runtime, params *ChangeNumApprovalsThresholdParams) *abi.EmptyValue {
   431  	// Can only be called by the multisig wallet itself.
   432  	rt.ValidateImmediateCallerIs(rt.Receiver())
   433  
   434  	var st State
   435  	rt.StateTransaction(&st, func() {
   436  		if params.NewThreshold == 0 || params.NewThreshold > uint64(len(st.Signers)) {
   437  			rt.Abortf(exitcode.ErrIllegalArgument, "New threshold value not supported")
   438  		}
   439  
   440  		st.NumApprovalsThreshold = params.NewThreshold
   441  	})
   442  	return nil
   443  }
   444  
   445  //type LockBalanceParams struct {
   446  //	StartEpoch abi.ChainEpoch
   447  //	UnlockDuration abi.ChainEpoch
   448  //	Amount abi.TokenAmount
   449  //}
   450  type LockBalanceParams = multisig0.LockBalanceParams
   451  
   452  func (a Actor) LockBalance(rt runtime.Runtime, params *LockBalanceParams) *abi.EmptyValue {
   453  	// Can only be called by the multisig wallet itself.
   454  	rt.ValidateImmediateCallerIs(rt.Receiver())
   455  
   456  	if params.UnlockDuration <= 0 {
   457  		// Note: Unlock duration of zero is workable, but rejected as ineffective, probably an error.
   458  		rt.Abortf(exitcode.ErrIllegalArgument, "unlock duration must be positive")
   459  	}
   460  
   461  	if params.Amount.LessThan(big.Zero()) {
   462  		rt.Abortf(exitcode.ErrIllegalArgument, "amount to lock must be positive")
   463  	}
   464  
   465  	var st State
   466  	rt.StateTransaction(&st, func() {
   467  		if st.UnlockDuration != 0 {
   468  			rt.Abortf(exitcode.ErrForbidden, "modification of unlock disallowed")
   469  		}
   470  		st.SetLocked(params.StartEpoch, params.UnlockDuration, params.Amount)
   471  	})
   472  	return nil
   473  }
   474  
   475  func (a Actor) approveTransaction(rt runtime.Runtime, txnID TxnID, txn *Transaction) (bool, []byte, exitcode.ExitCode) {
   476  	caller := rt.Caller()
   477  
   478  	var st State
   479  	// abort duplicate approval
   480  	for _, previousApprover := range txn.Approved {
   481  		if previousApprover == caller {
   482  			rt.Abortf(exitcode.ErrForbidden, "%s already approved this message", previousApprover)
   483  		}
   484  	}
   485  
   486  	// add the caller to the list of approvers
   487  	rt.StateTransaction(&st, func() {
   488  		ptx, err := adt.AsMap(adt.AsStore(rt), st.PendingTxns, builtin.DefaultHamtBitwidth)
   489  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load pending transactions")
   490  
   491  		// update approved on the transaction
   492  		txn.Approved = append(txn.Approved, caller)
   493  		err = ptx.Put(txnID, txn)
   494  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to put transaction %v for approval", txnID)
   495  
   496  		st.PendingTxns, err = ptx.Root()
   497  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush pending transactions")
   498  	})
   499  
   500  	return executeTransactionIfApproved(rt, st, txnID, txn)
   501  }
   502  
   503  func getTransaction(rt runtime.Runtime, ptx *adt.Map, txnID TxnID, proposalHash []byte, checkHash bool) *Transaction {
   504  	// get transaction from the state trie
   505  	var txn Transaction
   506  	found, err := ptx.Get(txnID, &txn)
   507  	builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load transaction %v for approval", txnID)
   508  	if !found {
   509  		rt.Abortf(exitcode.ErrNotFound, "no such transaction %v for approval", txnID)
   510  	}
   511  
   512  	// confirm the hashes match
   513  	if checkHash {
   514  		calculatedHash, err := ComputeProposalHash(&txn, rt.HashBlake2b)
   515  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to compute proposal hash for %v", txnID)
   516  		if proposalHash != nil && !bytes.Equal(proposalHash, calculatedHash[:]) {
   517  			rt.Abortf(exitcode.ErrIllegalArgument, "hash does not match proposal params (ensure requester is an ID address)")
   518  		}
   519  	}
   520  
   521  	return &txn
   522  }
   523  
   524  func executeTransactionIfApproved(rt runtime.Runtime, st State, txnID TxnID, txn *Transaction) (bool, []byte, exitcode.ExitCode) {
   525  	var out builtin.CBORBytes
   526  	var code exitcode.ExitCode
   527  	applied := false
   528  
   529  	thresholdMet := uint64(len(txn.Approved)) >= st.NumApprovalsThreshold
   530  	if thresholdMet {
   531  		if err := st.assertAvailable(rt.CurrentBalance(), txn.Value, rt.CurrEpoch()); err != nil {
   532  			rt.Abortf(exitcode.ErrInsufficientFunds, "insufficient funds unlocked: %v", err)
   533  		}
   534  
   535  		// A sufficient number of approvals have arrived and sufficient funds have been unlocked: relay the message and delete from pending queue.
   536  		code = rt.Send(
   537  			txn.To,
   538  			txn.Method,
   539  			builtin.CBORBytes(txn.Params),
   540  			txn.Value,
   541  			&out,
   542  		)
   543  		applied = true
   544  
   545  		// This could be rearranged to happen inside the first state transaction, before the send().
   546  		rt.StateTransaction(&st, func() {
   547  			ptx, err := adt.AsMap(adt.AsStore(rt), st.PendingTxns, builtin.DefaultHamtBitwidth)
   548  			builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load pending transactions")
   549  
   550  			// Allow transaction not to be found when deleting.
   551  			// This allows 1 out of n multisig swaps and removes initiated by the swapped/removed signer to go through cleanly.
   552  			if _, err := ptx.TryDelete(txnID); err != nil {
   553  				rt.Abortf(exitcode.ErrIllegalState, "failed to delete transaction for cleanup: %v", err)
   554  			}
   555  
   556  			st.PendingTxns, err = ptx.Root()
   557  			builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush pending transactions")
   558  		})
   559  	}
   560  
   561  	// Pass the return value through uninterpreted with the expectation that serializing into a CBORBytes never fails
   562  	// since it just copies the bytes.
   563  
   564  	return applied, out, code
   565  }
   566  
   567  // Computes a digest of a proposed transaction. This digest is used to confirm identity of the transaction
   568  // associated with an ID, which might change under chain re-orgs.
   569  func ComputeProposalHash(txn *Transaction, hash func([]byte) [32]byte) ([]byte, error) {
   570  	hashData := ProposalHashData{
   571  		Requester: txn.Approved[0],
   572  		To:        txn.To,
   573  		Value:     txn.Value,
   574  		Method:    txn.Method,
   575  		Params:    txn.Params,
   576  	}
   577  
   578  	data, err := hashData.Serialize()
   579  	if err != nil {
   580  		return nil, fmt.Errorf("failed to construct multisig approval hash: %w", err)
   581  	}
   582  
   583  	hashResult := hash(data)
   584  	return hashResult[:], nil
   585  }