This repository has been archived by the owner on Jun 6, 2023. It is now read-only.
/
multisig_state.go
157 lines (136 loc) · 5.1 KB
/
multisig_state.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package multisig
import (
address "github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/exitcode"
cid "github.com/ipfs/go-cid"
"golang.org/x/xerrors"
"github.com/filecoin-project/specs-actors/v2/actors/util/adt"
)
type State struct {
// Signers may be either public-key or actor ID-addresses. The ID address is canonical, but doesn't exist
// for a public key that has not yet received a message on chain.
// If any signer address is a public-key address, it will be resolved to an ID address and persisted
// in this state when the address is used.
Signers []address.Address
NumApprovalsThreshold uint64
NextTxnID TxnID
// Linear unlock
InitialBalance abi.TokenAmount
StartEpoch abi.ChainEpoch
UnlockDuration abi.ChainEpoch
PendingTxns cid.Cid // HAMT[TxnID]Transaction
}
func (st *State) SetLocked(startEpoch abi.ChainEpoch, unlockDuration abi.ChainEpoch, lockedAmount abi.TokenAmount) {
st.StartEpoch = startEpoch
st.UnlockDuration = unlockDuration
st.InitialBalance = lockedAmount
}
func (st *State) AmountLocked(elapsedEpoch abi.ChainEpoch) abi.TokenAmount {
if elapsedEpoch >= st.UnlockDuration {
return abi.NewTokenAmount(0)
}
if elapsedEpoch <= 0 {
return st.InitialBalance
}
unlockDuration := big.NewInt(int64(st.UnlockDuration))
remainingLockDuration := big.Sub(unlockDuration, big.NewInt(int64(elapsedEpoch)))
// locked = ceil(InitialBalance * remainingLockDuration / UnlockDuration)
numerator := big.Mul(st.InitialBalance, remainingLockDuration)
denominator := unlockDuration
quot := big.Div(numerator, denominator)
rem := big.Mod(numerator, denominator)
locked := quot
if !rem.IsZero() {
locked = big.Add(locked, big.NewInt(1))
}
return locked
}
// Iterates all pending transactions and removes an address from each list of approvals, if present.
// If an approval list becomes empty, the pending transaction is deleted.
func (st *State) PurgeApprovals(store adt.Store, addr address.Address) error {
txns, err := adt.AsMap(store, st.PendingTxns)
if err != nil {
return xerrors.Errorf("failed to load transactions: %w", err)
}
// Identify the transactions that need updating.
var txnIdsToPurge []string // For stable iteration
txnsToPurge := map[string]Transaction{} // Values are not pointers, we need copies
var txn Transaction
if err = txns.ForEach(&txn, func(txid string) error {
for _, approver := range txn.Approved {
if approver == addr {
txnIdsToPurge = append(txnIdsToPurge, txid)
txnsToPurge[txid] = txn
break
}
}
return nil
}); err != nil {
return xerrors.Errorf("failed to traverse transactions: %w", err)
}
// Update or remove those transactions.
for _, txid := range txnIdsToPurge {
txn := txnsToPurge[txid]
// The right length is almost certainly len-1, but let's not be too clever.
newApprovers := make([]address.Address, 0, len(txn.Approved))
for _, approver := range txn.Approved {
if approver != addr {
newApprovers = append(newApprovers, approver)
}
}
if len(newApprovers) > 0 {
txn.Approved = newApprovers
if err := txns.Put(StringKey(txid), &txn); err != nil {
return xerrors.Errorf("failed to update transaction approvers: %w", err)
}
} else {
if err := txns.Delete(StringKey(txid)); err != nil {
return xerrors.Errorf("failed to delete transaction with no approvers: %w", err)
}
}
}
if newTxns, err := txns.Root(); err != nil {
return xerrors.Errorf("failed to persist transactions: %w", err)
} else {
st.PendingTxns = newTxns
}
return nil
}
// return nil if MultiSig maintains required locked balance after spending the amount, else return an error.
func (st *State) assertAvailable(currBalance abi.TokenAmount, amountToSpend abi.TokenAmount, currEpoch abi.ChainEpoch) error {
if amountToSpend.LessThan(big.Zero()) {
return xerrors.Errorf("amount to spend %s less than zero", amountToSpend.String())
}
if currBalance.LessThan(amountToSpend) {
return xerrors.Errorf("current balance %s less than amount to spend %s", currBalance.String(), amountToSpend.String())
}
if amountToSpend.IsZero() {
// Always permit a transaction that sends no value, even if the lockup exceeds the current balance.
return nil
}
remainingBalance := big.Sub(currBalance, amountToSpend)
amountLocked := st.AmountLocked(currEpoch - st.StartEpoch)
if remainingBalance.LessThan(amountLocked) {
return xerrors.Errorf("balance %s if spent %s would be less than locked amount %s",
remainingBalance.String(), amountToSpend, amountLocked.String())
}
return nil
}
func getPendingTransaction(ptx *adt.Map, txnID TxnID) (Transaction, error) {
var out Transaction
found, err := ptx.Get(txnID, &out)
if err != nil {
return Transaction{}, xerrors.Errorf("failed to read transaction: %w", err)
}
if !found {
return Transaction{}, exitcode.ErrNotFound.Wrapf("failed to find transaction %v", txnID)
}
return out, nil
}
// An adt.Map key that just preserves the underlying string.
type StringKey string
func (k StringKey) Key() string {
return string(k)
}