Skip to content
This repository was archived by the owner on May 13, 2022. It is now read-only.

Commit 314357e

Browse files
author
Silas Davis
committed
This fixes two issues with our state handling when querying state via
`CallSim` and `CallCodeSim`. State is separated into a mutable write side and an immutable read side (from last commit/height) via RWTree and then via MutableForest in an MVCC style manner. Reads should hit the last committed version. However there were two bugs in TransactServer's `CallTxSim` and `CallCodeSim`: 1. During the execution of a simulated call if a block is committed reads can be made from consecutive blocks and so may not see a consistent view of the state. 2. `ImmutableForest`'s `loadOrCreateTree` was unsynchronised and committed a multitude of sins: creating and returning multiple trees for the same prefix when it should ensure one instance exists in cache and most problematically allowing access to uninitialised trees by pushing into LRU cache before `Load`ing the tree from DB. The fix for 1 is to pull a snapshot `ImmutableForest` when Call*Sim are called which is fixed to the latest height at the time of the call. The fix for 2 was to synchronise `loadOrCreateTree` and only push initialised trees into the `treeCache`. simulated_call_test.go adds a test that triggers the second bug (requires a cold cache and a prefix with previous existing versions) Additionally: - Make MutableForest and ImmutableForest fully thread-safe on reads and writes. Signed-off-by: Silas Davis <[email protected]>
1 parent aad850b commit 314357e

34 files changed

+676
-306
lines changed

acm/acmstate/state_cache.go

+20-15
Original file line numberDiff line numberDiff line change
@@ -262,21 +262,26 @@ func (cache *Cache) get(address crypto.Address) (*accountInfo, error) {
262262
cache.RLock()
263263
accInfo := cache.accounts[address]
264264
cache.RUnlock()
265-
if accInfo == nil {
266-
cache.Lock()
267-
defer cache.Unlock()
268-
accInfo = cache.accounts[address]
269-
if accInfo == nil {
270-
account, err := cache.backend.GetAccount(address)
271-
if err != nil {
272-
return nil, err
273-
}
274-
accInfo = &accountInfo{
275-
account: account,
276-
storage: make(map[binary.Word256][]byte),
277-
}
278-
cache.accounts[address] = accInfo
279-
}
265+
if accInfo != nil {
266+
return accInfo, nil
267+
}
268+
// Take write lock to fill cache
269+
cache.Lock()
270+
defer cache.Unlock()
271+
// Check for an interleaved cache fill
272+
accInfo = cache.accounts[address]
273+
if accInfo != nil {
274+
return accInfo, nil
275+
}
276+
// Pull from backend
277+
account, err := cache.backend.GetAccount(address)
278+
if err != nil {
279+
return nil, err
280+
}
281+
accInfo = &accountInfo{
282+
account: account,
283+
storage: make(map[binary.Word256][]byte),
280284
}
285+
cache.accounts[address] = accInfo
281286
return accInfo, nil
282287
}

cmd/burrow/commands/errors.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package commands
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/hyperledger/burrow/execution/errors"
7+
cli "github.com/jawher/mow.cli"
8+
)
9+
10+
func Errors(output Output) func(cmd *cli.Cmd) {
11+
return func(cmd *cli.Cmd) {
12+
13+
jsonOpt := cmd.BoolOpt("j json", false, "output errors as a JSON object")
14+
15+
cmd.Spec = "[ --json ]"
16+
17+
cmd.Action = func() {
18+
if *jsonOpt {
19+
bs, err := json.MarshalIndent(errors.Codes, "", "\t")
20+
if err != nil {
21+
output.Fatalf("Could not marshal error codes: %w", err)
22+
}
23+
output.Printf(string(bs))
24+
} else {
25+
output.Printf(errors.Codes.String())
26+
}
27+
}
28+
}
29+
}

cmd/burrow/main.go

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ func burrow(output commands.Output) *cli.Cli {
8787
app.Command("compile", "Compile solidity files embedding the compilation results as a fixture in a Go file",
8888
commands.Compile(output))
8989

90+
app.Command("errors", "Print error codes",
91+
commands.Errors(output))
9092
return app
9193
}
9294

core/processes.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"net/http"
77
"time"
88

9+
"github.com/hyperledger/burrow/acm/acmstate"
10+
911
"github.com/hyperledger/burrow/bcm"
1012
"github.com/hyperledger/burrow/consensus/abci"
1113
"github.com/hyperledger/burrow/execution"
@@ -320,7 +322,9 @@ func GRPCLauncher(kern *Kernel, conf *rpc.ServerConfig, keyConfig *keys.KeysConf
320322

321323
txCodec := txs.NewProtobufCodec()
322324
rpctransact.RegisterTransactServer(grpcServer,
323-
rpctransact.NewTransactServer(kern.State, kern.Blockchain, kern.Transactor, txCodec, kern.Logger))
325+
rpctransact.NewTransactServer(func() (acmstate.Reader, error) {
326+
return kern.State.AtLatestVersion()
327+
}, kern.Blockchain, kern.Transactor, txCodec, kern.Logger))
324328

325329
rpcevents.RegisterExecutionEventsServer(grpcServer, rpcevents.NewExecutionEventsServer(kern.State,
326330
kern.Emitter, kern.Blockchain, kern.Logger))

dump/dump.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (ds *Dumper) Transmit(sink Sink, startHeight, endHeight uint64, options Opt
7070
if endHeight == 0 || endHeight > lastHeight {
7171
endHeight = lastHeight
7272
}
73-
st, err := ds.state.LoadHeight(endHeight)
73+
st, err := ds.state.AtHeight(endHeight)
7474
if err != nil {
7575
return err
7676
}

execution/accounts.go

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func NewAccounts(reader acmstate.Reader, keyClient keys.KeyClient, mutexCount in
3636
keyClient: keyClient,
3737
}
3838
}
39+
3940
func (accs *Accounts) SigningAccount(address crypto.Address) (*SigningAccount, error) {
4041
signer, err := keys.AddressableSigner(accs.keyClient, address)
4142
if err != nil {

execution/errors/codes.go

+15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"encoding/json"
55
"fmt"
66
"reflect"
7+
"strconv"
8+
"strings"
79
)
810

911
type codes struct {
@@ -89,3 +91,16 @@ func (es *codes) Get(number uint32) *Code {
8991
}
9092
return es.codes[number]
9193
}
94+
95+
func (es *codes) String() string {
96+
sb := new(strings.Builder)
97+
for _, c := range es.codes {
98+
sb.WriteString(strconv.FormatUint(uint64(c.Number), 10))
99+
sb.WriteString(": ")
100+
sb.WriteString(c.Name)
101+
sb.WriteString(" - ")
102+
sb.WriteString(c.Description)
103+
sb.WriteRune('\n')
104+
}
105+
return sb.String()
106+
}

execution/errors/errors.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type Source interface {
2121
Error() error
2222
}
2323

24-
var Codes = codes{
24+
var Codes = &codes{
2525
None: code("none"),
2626
UnknownAddress: code("unknown address"),
2727
InsufficientBalance: code("insufficient balance"),

execution/errors/errors_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ func TestCode_String(t *testing.T) {
2525
err := Codes.CodeOutOfBounds
2626
fmt.Println(err.Error())
2727
}
28+
29+
func TestPrintCodes(t *testing.T) {
30+
fmt.Printf("%v", Codes)
31+
}

execution/execution.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type ExecutorState interface {
5454
proposal.Reader
5555
validator.IterableReader
5656
}
57+
5758
type BatchExecutor interface {
5859
// Provides access to write lock for a BatchExecutor so reads can be prevented for the duration of a commit
5960
sync.Locker
@@ -284,7 +285,7 @@ func (exe *executor) validateInputsAndStorePublicKeys(txEnv *txs.Envelope) error
284285
for s, in := range txEnv.Tx.GetInputs() {
285286
err := exe.updateSignatory(txEnv.Signatories[s])
286287
if err != nil {
287-
return fmt.Errorf("failed to update public key for input %v: %v", in.Address, err)
288+
return fmt.Errorf("failed to update public key for input %v: %w", in.Address, err)
288289
}
289290
acc, err := exe.stateCache.GetAccount(in.Address)
290291
if err != nil {

execution/execution_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ func TestSendPermission(t *testing.T) {
234234
tx = payload.NewSendTx()
235235
require.NoError(t, tx.AddInput(exe.stateCache, users[0].GetPublicKey(), 5))
236236
require.NoError(t, tx.AddInput(exe.stateCache, users[1].GetPublicKey(), 5))
237-
require.NoError(t, tx.AddOutput(users[2].GetAddress(), 10))
237+
tx.AddOutput(users[2].GetAddress(), 10)
238238
err = exe.signExecuteCommit(tx, users[:2]...)
239239
require.Error(t, err)
240240
}
@@ -1203,6 +1203,11 @@ func TestMerklePanic(t *testing.T) {
12031203
acc0 := getAccount(t, st, privAccounts[0].GetAddress())
12041204
acc1 := getAccount(t, st, privAccounts[1].GetAddress())
12051205

1206+
acc, err := st.GetAccount(acc0.Address)
1207+
1208+
require.NoError(t, err)
1209+
require.NotNil(t, acc)
1210+
12061211
// SendTx.
12071212
{
12081213
tx := &payload.SendTx{

execution/simulated_call.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
// Cannot be used to create new contracts
1919
func CallSim(reader acmstate.Reader, blockchain bcm.BlockchainInfo, fromAddress, address crypto.Address, data []byte,
2020
logger *logging.Logger) (*exec.TxExecution, error) {
21-
2221
cache := acmstate.NewCache(reader)
2322
exe := contexts.CallContext{
2423
VMS: vms.NewConnectedVirtualMachines(engine.Options{}),
@@ -29,14 +28,15 @@ func CallSim(reader acmstate.Reader, blockchain bcm.BlockchainInfo, fromAddress,
2928
Logger: logger,
3029
}
3130

32-
txe := exec.NewTxExecution(txs.Enclose(blockchain.ChainID(), &payload.CallTx{
33-
Input: &payload.TxInput{
34-
Address: fromAddress,
35-
},
36-
Address: &address,
37-
Data: data,
38-
GasLimit: contexts.GasLimit,
39-
}))
31+
txe := exec.NewTxExecution(txs.Enclose(blockchain.ChainID(),
32+
&payload.CallTx{
33+
Input: &payload.TxInput{
34+
Address: fromAddress,
35+
},
36+
Address: &address,
37+
Data: data,
38+
GasLimit: contexts.GasLimit,
39+
}))
4040

4141
// Set height for downstream synchronisation purposes
4242
txe.Height = blockchain.LastBlockHeight()

execution/simulated_call_test.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package execution
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/big"
7+
"testing"
8+
9+
"github.com/hyperledger/burrow/acm"
10+
"github.com/hyperledger/burrow/acm/acmstate"
11+
"github.com/hyperledger/burrow/bcm"
12+
"github.com/hyperledger/burrow/crypto"
13+
"github.com/hyperledger/burrow/execution/engine"
14+
"github.com/hyperledger/burrow/execution/evm"
15+
"github.com/hyperledger/burrow/execution/evm/abi"
16+
"github.com/hyperledger/burrow/execution/exec"
17+
"github.com/hyperledger/burrow/execution/solidity"
18+
"github.com/hyperledger/burrow/execution/state"
19+
"github.com/hyperledger/burrow/genesis"
20+
"github.com/hyperledger/burrow/permission"
21+
"github.com/stretchr/testify/require"
22+
dbm "github.com/tendermint/tm-db"
23+
"golang.org/x/sync/errgroup"
24+
)
25+
26+
var genesisDoc, _, _ = genesis.NewDeterministicGenesis(100).GenesisDoc(1, 1)
27+
28+
// This test looks at caching problems that arise when doing concurrent reads via CallSim. It requires a cold cache
29+
// for bug to be exhibited.
30+
// The root cause of the original bug was a race condition that could lead to reading a uninitialised tree from the
31+
// MutableForest tree cache.
32+
func TestCallSimDelegate(t *testing.T) {
33+
// Roll up our sleeves and swear fealty to the witch-king
34+
ctx, cancel := context.WithCancel(context.Background())
35+
defer cancel()
36+
g, ctx := errgroup.WithContext(ctx)
37+
38+
db := dbm.NewMemDB()
39+
st, err := state.MakeGenesisState(db, genesisDoc)
40+
require.NoError(t, err)
41+
42+
from := crypto.PrivateKeyFromSecret("raaah", crypto.CurveTypeEd25519)
43+
contractAddress := crypto.Address{1, 2, 3, 4, 5}
44+
blockchain := &bcm.Blockchain{}
45+
sink := exec.NewNoopEventSink()
46+
47+
// Function to set storage value for later
48+
setDelegate := func(up state.Updatable, value crypto.Address) error {
49+
call, _, err := abi.EncodeFunctionCall(string(solidity.Abi_DelegateProxy), "setDelegate", logger, value)
50+
if err != nil {
51+
return err
52+
}
53+
54+
cache := acmstate.NewCache(st)
55+
_, err = evm.Default().Execute(cache, blockchain, sink,
56+
engine.CallParams{
57+
CallType: exec.CallTypeCall,
58+
Origin: from.GetAddress(),
59+
Caller: from.GetAddress(),
60+
Callee: contractAddress,
61+
Input: call,
62+
Gas: big.NewInt(9999999),
63+
}, solidity.DeployedBytecode_DelegateProxy)
64+
65+
if err != nil {
66+
return err
67+
}
68+
return cache.Sync(up)
69+
}
70+
71+
// Initialise sender smart contract state
72+
_, _, err = st.Update(func(up state.Updatable) error {
73+
err = up.UpdateAccount(&acm.Account{
74+
Address: from.GetAddress(),
75+
PublicKey: from.GetPublicKey(),
76+
Balance: 9999999,
77+
Permissions: permission.DefaultAccountPermissions,
78+
})
79+
if err != nil {
80+
return err
81+
}
82+
return up.UpdateAccount(&acm.Account{
83+
Address: contractAddress,
84+
EVMCode: solidity.DeployedBytecode_DelegateProxy,
85+
})
86+
})
87+
require.NoError(t, err)
88+
89+
// Set a series of values of storage slot so we get a deep version tree (which we need to trigger the bug)
90+
delegate := crypto.Address{0xBE, 0xEF, 0, 0xFA, 0xCE, 0, 0xBA, 0}
91+
for i := 0; i < 0xBF; i++ {
92+
delegate[7] = byte(i)
93+
_, _, err = st.Update(func(up state.Updatable) error {
94+
return setDelegate(up, delegate)
95+
})
96+
require.NoError(t, err)
97+
}
98+
99+
// This is important in order to illicit the former bug - we need a cold LRU tree cache in MutableForest
100+
st, err = state.LoadState(db, st.Version())
101+
require.NoError(t, err)
102+
103+
getIntCall, _, err := abi.EncodeFunctionCall(string(solidity.Abi_DelegateProxy), "getDelegate", logger)
104+
require.NoError(t, err)
105+
n := 1000
106+
107+
for i := 0; i < n; i++ {
108+
g.Go(func() error {
109+
txe, err := CallSim(st, blockchain, from.GetAddress(), contractAddress, getIntCall, logger)
110+
if err != nil {
111+
return err
112+
}
113+
err = txe.GetException().AsError()
114+
if err != nil {
115+
return err
116+
}
117+
address, err := crypto.AddressFromBytes(txe.GetResult().Return[12:])
118+
if err != nil {
119+
return err
120+
}
121+
if address != delegate {
122+
// The bug for which this test was written will return the zero address here since it is accessing
123+
// an uninitialised tree
124+
return fmt.Errorf("getDelegate returned %v but expected %v", address, delegate)
125+
}
126+
return nil
127+
})
128+
}
129+
130+
require.NoError(t, g.Wait())
131+
}

execution/solidity/delgate_proxy.sol

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
pragma solidity ^0.5;
2+
3+
contract DelegateProxy {
4+
address internal proxied;
5+
6+
function setDelegate(address _proxied) public {
7+
proxied = _proxied;
8+
}
9+
10+
function getDelegate() public view returns (address) {
11+
return proxied;
12+
}
13+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package solidity
2+
3+
import hex "github.com/tmthrgd/go-hex"
4+
5+
var Bytecode_DelegateProxy = hex.MustDecodeString("608060405234801561001057600080fd5b5061016a806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063bc7f3b501461003b578063ca5eb5e114610085575b600080fd5b6100436100c9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100c76004803603602081101561009b57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506100f2565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505056fea265627a7a7231582061c5d5f36c9a31fab0943b47e1f33f7c79322c9e41c8fa02eec89de42114050f64736f6c634300050f0032")
6+
var DeployedBytecode_DelegateProxy = hex.MustDecodeString("608060405234801561001057600080fd5b50600436106100365760003560e01c8063bc7f3b501461003b578063ca5eb5e114610085575b600080fd5b6100436100c9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100c76004803603602081101561009b57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506100f2565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505056fea265627a7a7231582061c5d5f36c9a31fab0943b47e1f33f7c79322c9e41c8fa02eec89de42114050f64736f6c634300050f0032")
7+
var Abi_DelegateProxy = []byte(`[{"constant":true,"inputs":[],"name":"getDelegate","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_proxied","type":"address"}],"name":"setDelegate","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]`)

0 commit comments

Comments
 (0)