Skip to content

Commit a25ca2f

Browse files
authored
feat: concurrent checkTx (#141)
* chore: bump up ostracon, iavl and tm-db * feat: concurrent checkTx (#49) * feat: implement new abci, `BeginRecheckTx()` and `EndRecheckTx()` * test: fix tests * refactor: decompose checkTx & runTx * chore: protect app.checkState w/ RWMutex for simulate * chore: remove unused var * feat: account lock decorator * chore: skip AccountLockDecorator if not checkTx * chore: bump up tendermint * chore: revise accountlock position * chore: accountlock_test * chore: revise accountlock covers `cache.Write()` * chore: revise `sampleBytes` to `2` * fix: test according to `sampleBytes` * chore: revise `getUniqSortedAddressKey()` and add `getAddressKey()` * chore: revise `how to sort` not to use `reflection` * chore: bump up tendermint * test: check `sorted` in `TestGetUniqSortedAddressKey()` * chore: move `accountLock` from `anteTx()` to `checkTx()` # Conflicts: # baseapp/abci.go # baseapp/baseapp.go # baseapp/baseapp_test.go # baseapp/helpers.go # go.mod # go.sum # x/bank/bench_test.go # x/mock/test_utils.go * fix: make it buildable * fix: tests * fix: gasWanted & gasUsed are always `0` (#51) * fix: gasWanted & gasUsed is always `0` * chore: error log for general panic # Conflicts: # baseapp/baseapp.go
1 parent a70834a commit a25ca2f

13 files changed

+394
-151
lines changed

baseapp/abci.go

+28-25
Original file line numberDiff line numberDiff line change
@@ -213,33 +213,38 @@ func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBloc
213213
func (app *BaseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
214214
defer telemetry.MeasureSince(time.Now(), "abci", "check_tx")
215215

216-
var mode runTxMode
217-
218-
switch {
219-
case req.Type == abci.CheckTxType_New:
220-
mode = runTxModeCheck
221-
222-
case req.Type == abci.CheckTxType_Recheck:
223-
mode = runTxModeReCheck
216+
tx, err := app.txDecoder(req.Tx)
217+
if err != nil {
218+
return sdkerrors.ResponseCheckTx(err, 0, 0, app.trace)
219+
}
224220

225-
default:
221+
if req.Type != abci.CheckTxType_New && req.Type != abci.CheckTxType_Recheck {
226222
panic(fmt.Sprintf("unknown RequestCheckTx type: %s", req.Type))
227223
}
228224

229-
gInfo, result, err := app.runTx(mode, req.Tx)
225+
gInfo, err := app.checkTx(req.Tx, tx, req.Type == abci.CheckTxType_Recheck)
230226
if err != nil {
231227
return sdkerrors.ResponseCheckTx(err, gInfo.GasWanted, gInfo.GasUsed, app.trace)
232228
}
233229

234230
return abci.ResponseCheckTx{
235231
GasWanted: int64(gInfo.GasWanted), // TODO: Should type accept unsigned ints?
236232
GasUsed: int64(gInfo.GasUsed), // TODO: Should type accept unsigned ints?
237-
Log: result.Log,
238-
Data: result.Data,
239-
Events: sdk.MarkEventsToIndex(result.Events, app.indexEvents),
240233
}
241234
}
242235

236+
// BeginRecheckTx implements the ABCI interface and set the check state based on the given header
237+
func (app *BaseApp) BeginRecheckTx(req abci.RequestBeginRecheckTx) abci.ResponseBeginRecheckTx {
238+
// NOTE: This is safe because Ostracon holds a lock on the mempool for Rechecking.
239+
app.setCheckState(req.Header)
240+
return abci.ResponseBeginRecheckTx{Code: abci.CodeTypeOK}
241+
}
242+
243+
// EndRecheckTx implements the ABCI interface.
244+
func (app *BaseApp) EndRecheckTx(req abci.RequestEndRecheckTx) abci.ResponseEndRecheckTx {
245+
return abci.ResponseEndRecheckTx{Code: abci.CodeTypeOK}
246+
}
247+
243248
// DeliverTx implements the ABCI interface and executes a tx in DeliverTx mode.
244249
// State only gets persisted if all messages are valid and get executed successfully.
245250
// Otherwise, the ResponseDeliverTx will contain releveant error information.
@@ -258,7 +263,12 @@ func (app *BaseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx
258263
telemetry.SetGauge(float32(gInfo.GasWanted), "tx", "gas", "wanted")
259264
}()
260265

261-
gInfo, result, err := app.runTx(runTxModeDeliver, req.Tx)
266+
tx, err := app.txDecoder(req.Tx)
267+
if err != nil {
268+
return sdkerrors.ResponseDeliverTx(err, 0, 0, app.trace)
269+
}
270+
271+
gInfo, result, err := app.runTx(req.Tx, tx, false)
262272
if err != nil {
263273
resultStr = "failed"
264274
return sdkerrors.ResponseDeliverTx(err, gInfo.GasWanted, gInfo.GasUsed, app.trace)
@@ -275,11 +285,10 @@ func (app *BaseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx
275285

276286
// Commit implements the ABCI interface. It will commit all state that exists in
277287
// the deliver state's multi-store and includes the resulting commit ID in the
278-
// returned abci.ResponseCommit. Commit will set the check state based on the
279-
// latest header and reset the deliver state. Also, if a non-zero halt height is
280-
// defined in config, Commit will execute a deferred function call to check
281-
// against that height and gracefully halt if it matches the latest committed
282-
// height.
288+
// returned abci.ResponseCommit. Commit will reset the deliver state.
289+
// Also, if a non-zero halt height is defined in config, Commit will execute
290+
// a deferred function call to check against that height and gracefully halt if
291+
// it matches the latest committed height.
283292
func (app *BaseApp) Commit() (res abci.ResponseCommit) {
284293
defer telemetry.MeasureSince(time.Now(), "abci", "commit")
285294

@@ -293,12 +302,6 @@ func (app *BaseApp) Commit() (res abci.ResponseCommit) {
293302
commitID := app.cms.Commit()
294303
app.logger.Info("commit synced", "commit", fmt.Sprintf("%X", commitID))
295304

296-
// Reset the Check state to the latest committed.
297-
//
298-
// NOTE: This is safe because Tendermint holds a lock on the mempool for
299-
// Commit. Use the header from this latest block.
300-
app.setCheckState(header)
301-
302305
// empty/reset the deliver state
303306
app.deliverState = nil
304307

baseapp/accountlock.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package baseapp
2+
3+
import (
4+
"encoding/binary"
5+
"sort"
6+
"sync"
7+
8+
sdk "github.com/line/lbm-sdk/v2/types"
9+
)
10+
11+
// NOTE should 1 <= sampleBytes <= 4. If modify it, you should revise `getAddressKey()` as well
12+
const sampleBytes = 2
13+
14+
type AccountLock struct {
15+
accMtx [1 << (sampleBytes * 8)]sync.Mutex
16+
}
17+
18+
func (al *AccountLock) Lock(ctx sdk.Context, tx sdk.Tx) []uint32 {
19+
if !ctx.IsCheckTx() || ctx.IsReCheckTx() {
20+
return nil
21+
}
22+
23+
signers := getSigners(tx)
24+
accKeys := getUniqSortedAddressKey(signers)
25+
26+
for _, key := range accKeys {
27+
al.accMtx[key].Lock()
28+
}
29+
30+
return accKeys
31+
}
32+
33+
func (al *AccountLock) Unlock(accKeys []uint32) {
34+
// NOTE reverse order
35+
for i, length := 0, len(accKeys); i < length; i++ {
36+
key := accKeys[length-1-i]
37+
al.accMtx[key].Unlock()
38+
}
39+
}
40+
41+
func getSigners(tx sdk.Tx) []sdk.AccAddress {
42+
seen := map[string]bool{}
43+
var signers []sdk.AccAddress
44+
for _, msg := range tx.GetMsgs() {
45+
for _, addr := range msg.GetSigners() {
46+
if !seen[addr.String()] {
47+
signers = append(signers, addr)
48+
seen[addr.String()] = true
49+
}
50+
}
51+
}
52+
return signers
53+
}
54+
55+
func getUniqSortedAddressKey(addrs []sdk.AccAddress) []uint32 {
56+
accKeys := make([]uint32, 0, len(addrs))
57+
for _, addr := range addrs {
58+
accKeys = append(accKeys, getAddressKey(addr))
59+
}
60+
61+
accKeys = uniq(accKeys)
62+
sort.Sort(uint32Slice(accKeys))
63+
64+
return accKeys
65+
}
66+
67+
func getAddressKey(addr sdk.AccAddress) uint32 {
68+
return uint32(binary.BigEndian.Uint16(addr))
69+
}
70+
71+
func uniq(u []uint32) []uint32 {
72+
seen := map[uint32]bool{}
73+
var ret []uint32
74+
for _, v := range u {
75+
if !seen[v] {
76+
ret = append(ret, v)
77+
seen[v] = true
78+
}
79+
}
80+
return ret
81+
}
82+
83+
// Uint32Slice attaches the methods of Interface to []uint32, sorting in increasing order.
84+
type uint32Slice []uint32
85+
86+
func (p uint32Slice) Len() int { return len(p) }
87+
func (p uint32Slice) Less(i, j int) bool { return p[i] < p[j] }
88+
func (p uint32Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

baseapp/accountlock_test.go

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package baseapp
2+
3+
import (
4+
"reflect"
5+
"sort"
6+
"sync"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
ostproto "github.com/line/ostracon/proto/ostracon/types"
12+
13+
"github.com/line/lbm-sdk/v2/crypto/keys/secp256k1"
14+
"github.com/line/lbm-sdk/v2/testutil/testdata"
15+
sdk "github.com/line/lbm-sdk/v2/types"
16+
)
17+
18+
func TestAccountLock(t *testing.T) {
19+
app := setupBaseApp(t)
20+
ctx := app.NewContext(true, ostproto.Header{})
21+
22+
privs := newTestPrivKeys(3)
23+
tx := newTestTx(privs)
24+
25+
accKeys := app.accountLock.Lock(ctx, tx)
26+
27+
for _, accKey := range accKeys {
28+
require.True(t, isMutexLock(&app.accountLock.accMtx[accKey]))
29+
}
30+
31+
app.accountLock.Unlock(accKeys)
32+
33+
for _, accKey := range accKeys {
34+
require.False(t, isMutexLock(&app.accountLock.accMtx[accKey]))
35+
}
36+
}
37+
38+
func TestUnlockDoNothingWithNil(t *testing.T) {
39+
app := setupBaseApp(t)
40+
require.NotPanics(t, func() { app.accountLock.Unlock(nil) })
41+
}
42+
43+
func TestGetSigner(t *testing.T) {
44+
privs := newTestPrivKeys(3)
45+
tx := newTestTx(privs)
46+
signers := getSigners(tx)
47+
48+
require.Equal(t, getAddrs(privs), signers)
49+
}
50+
51+
func TestGetUniqSortedAddressKey(t *testing.T) {
52+
privs := newTestPrivKeys(3)
53+
54+
addrs := getAddrs(privs)
55+
addrs = append(addrs, addrs[1], addrs[0])
56+
require.Equal(t, 5, len(addrs))
57+
58+
accKeys := getUniqSortedAddressKey(addrs)
59+
60+
// length should be reduced because `duplicated` is removed
61+
require.Less(t, len(accKeys), len(addrs))
62+
63+
// check uniqueness
64+
for i, iv := range accKeys {
65+
for j, jv := range accKeys {
66+
if i != j {
67+
require.True(t, iv != jv)
68+
}
69+
}
70+
}
71+
72+
// should be sorted
73+
require.True(t, sort.IsSorted(uint32Slice(accKeys)))
74+
}
75+
76+
type AccountLockTestTx struct {
77+
Msgs []sdk.Msg
78+
}
79+
80+
var _ sdk.Tx = AccountLockTestTx{}
81+
82+
func (tx AccountLockTestTx) GetMsgs() []sdk.Msg {
83+
return tx.Msgs
84+
}
85+
86+
func (tx AccountLockTestTx) ValidateBasic() error {
87+
return nil
88+
}
89+
90+
func newTestPrivKeys(num int) []*secp256k1.PrivKey {
91+
privs := make([]*secp256k1.PrivKey, 0, num)
92+
for i := 0; i < num; i++ {
93+
privs = append(privs, secp256k1.GenPrivKey())
94+
}
95+
return privs
96+
}
97+
98+
func getAddrs(privs []*secp256k1.PrivKey) []sdk.AccAddress {
99+
addrs := make([]sdk.AccAddress, 0, len(privs))
100+
for _, priv := range privs {
101+
addrs = append(addrs, sdk.AccAddress(priv.PubKey().Address()))
102+
}
103+
return addrs
104+
}
105+
106+
func newTestTx(privs []*secp256k1.PrivKey) sdk.Tx {
107+
addrs := getAddrs(privs)
108+
msgs := make([]sdk.Msg, len(addrs))
109+
for i, addr := range addrs {
110+
msgs[i] = testdata.NewTestMsg(addr)
111+
}
112+
return AccountLockTestTx{Msgs: msgs}
113+
}
114+
115+
// Hack (too slow)
116+
func isMutexLock(mtx *sync.Mutex) bool {
117+
state := reflect.ValueOf(mtx).Elem().FieldByName("state")
118+
return state.Int() == 1
119+
}

0 commit comments

Comments
 (0)