github.com/koko1123/flow-go-1@v0.29.6/fvm/environment/programs_test.go (about)

     1  package environment_test
     2  
     3  import (
     4  	"encoding/hex"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/onflow/cadence/runtime/common"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/koko1123/flow-go-1/engine/execution/state/delta"
    12  	"github.com/koko1123/flow-go-1/fvm"
    13  	"github.com/koko1123/flow-go-1/fvm/derived"
    14  	"github.com/koko1123/flow-go-1/fvm/environment"
    15  	"github.com/koko1123/flow-go-1/fvm/state"
    16  	"github.com/koko1123/flow-go-1/model/flow"
    17  )
    18  
    19  func Test_Programs(t *testing.T) {
    20  
    21  	addressA := flow.HexToAddress("0a")
    22  	addressB := flow.HexToAddress("0b")
    23  	addressC := flow.HexToAddress("0c")
    24  
    25  	contractALocation := common.AddressLocation{
    26  		Address: common.Address(addressA),
    27  		Name:    "A",
    28  	}
    29  
    30  	contractBLocation := common.AddressLocation{
    31  		Address: common.Address(addressB),
    32  		Name:    "B",
    33  	}
    34  
    35  	contractCLocation := common.AddressLocation{
    36  		Address: common.Address(addressC),
    37  		Name:    "C",
    38  	}
    39  
    40  	contractA0Code := `
    41  		pub contract A {
    42  			pub fun hello(): String {
    43          		return "bad version"
    44      		}
    45  		}
    46  	`
    47  
    48  	contractACode := `
    49  		pub contract A {
    50  			pub fun hello(): String {
    51          		return "hello from A"
    52      		}
    53  		}
    54  	`
    55  
    56  	contractBCode := `
    57  		import A from 0xa
    58  	
    59  		pub contract B {
    60  			pub fun hello(): String {
    61         		return "hello from B but also ".concat(A.hello())
    62      		}
    63  		}
    64  	`
    65  
    66  	contractCCode := `
    67  		import B from 0xb
    68  	
    69  		pub contract C {
    70  			pub fun hello(): String {
    71  	   		return "hello from C, ".concat(B.hello())
    72  			}
    73  		}
    74  	`
    75  
    76  	callTx := func(name string, address flow.Address) *flow.TransactionBody {
    77  
    78  		return flow.NewTransactionBody().SetScript([]byte(fmt.Sprintf(`
    79  			import %s from %s
    80  			transaction {
    81                prepare() {
    82                  log(%s.hello())
    83                }
    84              }`, name, address.HexWithPrefix(), name)),
    85  		)
    86  	}
    87  
    88  	contractDeployTx := func(name, code string, address flow.Address) *flow.TransactionBody {
    89  		encoded := hex.EncodeToString([]byte(code))
    90  
    91  		return flow.NewTransactionBody().SetScript([]byte(fmt.Sprintf(`transaction {
    92                prepare(signer: AuthAccount) {
    93                  signer.contracts.add(name: "%s", code: "%s".decodeHex())
    94                }
    95              }`, name, encoded)),
    96  		).AddAuthorizer(address)
    97  	}
    98  
    99  	updateContractTx := func(name, code string, address flow.Address) *flow.TransactionBody {
   100  		encoded := hex.EncodeToString([]byte(code))
   101  
   102  		return flow.NewTransactionBody().SetScript([]byte(fmt.Sprintf(`transaction {
   103               prepare(signer: AuthAccount) {
   104                 signer.contracts.update__experimental(name: "%s", code: "%s".decodeHex())
   105               }
   106             }`, name, encoded)),
   107  		).AddAuthorizer(address)
   108  	}
   109  
   110  	mainView := delta.NewView(func(_, _ string) (flow.RegisterValue, error) {
   111  		return nil, nil
   112  	})
   113  
   114  	txnState := state.NewTransactionState(mainView, state.DefaultParameters())
   115  
   116  	vm := fvm.NewVirtualMachine()
   117  	derivedBlockData := derived.NewEmptyDerivedBlockData()
   118  
   119  	accounts := environment.NewAccounts(txnState)
   120  
   121  	err := accounts.Create(nil, addressA)
   122  	require.NoError(t, err)
   123  
   124  	err = accounts.Create(nil, addressB)
   125  	require.NoError(t, err)
   126  
   127  	err = accounts.Create(nil, addressC)
   128  	require.NoError(t, err)
   129  
   130  	// err = stm.
   131  	require.NoError(t, err)
   132  
   133  	fmt.Printf("Account created\n")
   134  
   135  	context := fvm.NewContext(
   136  		fvm.WithContractDeploymentRestricted(false),
   137  		fvm.WithAuthorizationChecksEnabled(false),
   138  		fvm.WithSequenceNumberCheckAndIncrementEnabled(false),
   139  		fvm.WithCadenceLogging(true),
   140  		fvm.WithDerivedBlockData(derivedBlockData))
   141  
   142  	var contractAView *delta.View = nil
   143  	var contractBView *delta.View = nil
   144  	var txAView *delta.View = nil
   145  
   146  	t.Run("contracts can be updated", func(t *testing.T) {
   147  		retrievedContractA, err := accounts.GetContract("A", addressA)
   148  		require.NoError(t, err)
   149  		require.Empty(t, retrievedContractA)
   150  
   151  		// deploy contract A0
   152  		procContractA0 := fvm.Transaction(
   153  			contractDeployTx("A", contractA0Code, addressA),
   154  			derivedBlockData.NextTxIndexForTestingOnly())
   155  		err = vm.Run(context, procContractA0, mainView)
   156  		require.NoError(t, err)
   157  
   158  		retrievedContractA, err = accounts.GetContract("A", addressA)
   159  		require.NoError(t, err)
   160  
   161  		require.Equal(t, contractA0Code, string(retrievedContractA))
   162  
   163  		// deploy contract A
   164  		procContractA := fvm.Transaction(
   165  			updateContractTx("A", contractACode, addressA),
   166  			derivedBlockData.NextTxIndexForTestingOnly())
   167  		err = vm.Run(context, procContractA, mainView)
   168  		require.NoError(t, err)
   169  		require.NoError(t, procContractA.Err)
   170  
   171  		retrievedContractA, err = accounts.GetContract("A", addressA)
   172  		require.NoError(t, err)
   173  
   174  		require.Equal(t, contractACode, string(retrievedContractA))
   175  
   176  	})
   177  
   178  	t.Run("register touches are captured for simple contract A", func(t *testing.T) {
   179  
   180  		// deploy contract A
   181  		procContractA := fvm.Transaction(
   182  			contractDeployTx("A", contractACode, addressA),
   183  			derivedBlockData.NextTxIndexForTestingOnly())
   184  		err := vm.Run(context, procContractA, mainView)
   185  		require.NoError(t, err)
   186  
   187  		fmt.Println("---------- Real transaction here ------------")
   188  
   189  		// run a TX using contract A
   190  		procCallA := fvm.Transaction(
   191  			callTx("A", addressA),
   192  			derivedBlockData.NextTxIndexForTestingOnly())
   193  
   194  		loadedCode := false
   195  		viewExecA := delta.NewView(func(owner, key string) (flow.RegisterValue, error) {
   196  			if key == environment.ContractKey("A") {
   197  				loadedCode = true
   198  			}
   199  
   200  			return mainView.Peek(owner, key)
   201  		})
   202  
   203  		err = vm.Run(context, procCallA, viewExecA)
   204  		require.NoError(t, err)
   205  
   206  		// make sure tx was really run
   207  		require.Contains(t, procCallA.Logs, "\"hello from A\"")
   208  
   209  		// Make sure the code has been loaded from storage
   210  		require.True(t, loadedCode)
   211  
   212  		entry := derivedBlockData.GetProgramForTestingOnly(contractALocation)
   213  		require.NotNil(t, entry)
   214  
   215  		// assert dependencies are correct
   216  		require.Len(t, entry.Value.Dependencies, 1)
   217  		require.NotNil(t, entry.Value.Dependencies[common.MustBytesToAddress(addressA.Bytes())])
   218  
   219  		// type assertion for further inspections
   220  		require.IsType(t, entry.State.View(), &delta.View{})
   221  
   222  		// assert some reads were recorded (at least loading of code)
   223  		deltaView := entry.State.View().(*delta.View)
   224  		require.NotEmpty(t, deltaView.Interactions().Reads)
   225  
   226  		contractAView = deltaView
   227  		txAView = viewExecA
   228  
   229  		// merge it back
   230  		err = mainView.MergeView(viewExecA)
   231  		require.NoError(t, err)
   232  
   233  		// execute transaction again, this time make sure it doesn't load code
   234  		viewExecA2 := delta.NewView(func(owner, key string) (flow.RegisterValue, error) {
   235  			// this time we fail if a read of code occurs
   236  			require.NotEqual(t, key, environment.ContractKey("A"))
   237  
   238  			return mainView.Peek(owner, key)
   239  		})
   240  
   241  		procCallA = fvm.Transaction(
   242  			callTx("A", addressA),
   243  			derivedBlockData.NextTxIndexForTestingOnly())
   244  
   245  		err = vm.Run(context, procCallA, viewExecA2)
   246  		require.NoError(t, err)
   247  
   248  		require.Contains(t, procCallA.Logs, "\"hello from A\"")
   249  
   250  		// same transaction should produce the exact same views
   251  		// but only because we don't do any conditional update in a tx
   252  		compareViews(t, viewExecA, viewExecA2)
   253  
   254  		// merge it back
   255  		err = mainView.MergeView(viewExecA2)
   256  		require.NoError(t, err)
   257  	})
   258  
   259  	t.Run("deploying another contract invalidates dependant programs", func(t *testing.T) {
   260  
   261  		// deploy contract B
   262  		procContractB := fvm.Transaction(
   263  			contractDeployTx("B", contractBCode, addressB),
   264  			derivedBlockData.NextTxIndexForTestingOnly())
   265  		err := vm.Run(context, procContractB, mainView)
   266  		require.NoError(t, err)
   267  
   268  		// b and c are invalid
   269  		entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation)
   270  		entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation)
   271  		// a is still valid
   272  		entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation)
   273  
   274  		require.Nil(t, entryB)
   275  		require.Nil(t, entryC)
   276  		require.NotNil(t, entryA)
   277  	})
   278  
   279  	var viewExecB *delta.View
   280  
   281  	t.Run("contract B imports contract A", func(t *testing.T) {
   282  
   283  		// programs should have no entries for A and B, as per previous test
   284  
   285  		// run a TX using contract B
   286  		procCallB := fvm.Transaction(
   287  			callTx("B", addressB),
   288  			derivedBlockData.NextTxIndexForTestingOnly())
   289  
   290  		viewExecB = delta.NewView(mainView.Peek)
   291  
   292  		err = vm.Run(context, procCallB, viewExecB)
   293  		require.NoError(t, err)
   294  
   295  		require.Contains(t, procCallB.Logs, "\"hello from B but also hello from A\"")
   296  
   297  		entry := derivedBlockData.GetProgramForTestingOnly(contractALocation)
   298  		require.NotNil(t, entry)
   299  
   300  		// state should be essentially the same as one which we got in tx with contract A
   301  		require.IsType(t, entry.State.View(), &delta.View{})
   302  		deltaA := entry.State.View().(*delta.View)
   303  
   304  		compareViews(t, contractAView, deltaA)
   305  
   306  		entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation)
   307  		require.NotNil(t, entryB)
   308  
   309  		// assert dependencies are correct
   310  		require.Len(t, entryB.Value.Dependencies, 2)
   311  		require.NotNil(t, entryB.Value.Dependencies[common.MustBytesToAddress(addressA.Bytes())])
   312  		require.NotNil(t, entryB.Value.Dependencies[common.MustBytesToAddress(addressB.Bytes())])
   313  
   314  		// program B should contain all the registers used by program A, as it depends on it
   315  		require.IsType(t, entryB.State.View(), &delta.View{})
   316  		deltaB := entryB.State.View().(*delta.View)
   317  
   318  		idsA, valuesA := deltaA.Delta().RegisterUpdates()
   319  		for i, id := range idsA {
   320  			v, has := deltaB.Delta().Get(id.Owner, id.Key)
   321  			require.True(t, has)
   322  
   323  			require.Equal(t, valuesA[i], v)
   324  		}
   325  
   326  		for id, registerA := range deltaA.Interactions().Reads {
   327  
   328  			registerB, has := deltaB.Interactions().Reads[id]
   329  			require.True(t, has)
   330  
   331  			require.Equal(t, registerA, registerB)
   332  		}
   333  
   334  		contractBView = deltaB
   335  
   336  		// merge it back
   337  		err = mainView.MergeView(viewExecB)
   338  		require.NoError(t, err)
   339  
   340  		// rerun transaction
   341  
   342  		// execute transaction again, this time make sure it doesn't load code
   343  		viewExecB2 := delta.NewView(func(owner, key string) (flow.RegisterValue, error) {
   344  			// this time we fail if a read of code occurs
   345  			require.NotEqual(t, key, environment.ContractKey("A"))
   346  			require.NotEqual(t, key, environment.ContractKey("B"))
   347  
   348  			return mainView.Peek(owner, key)
   349  		})
   350  
   351  		procCallB = fvm.Transaction(
   352  			callTx("B", addressB),
   353  			derivedBlockData.NextTxIndexForTestingOnly())
   354  
   355  		err = vm.Run(context, procCallB, viewExecB2)
   356  		require.NoError(t, err)
   357  
   358  		require.Contains(t, procCallB.Logs, "\"hello from B but also hello from A\"")
   359  
   360  		compareViews(t, viewExecB, viewExecB2)
   361  
   362  		// merge it back
   363  		err = mainView.MergeView(viewExecB2)
   364  		require.NoError(t, err)
   365  	})
   366  
   367  	t.Run("contract A runs from cache after program B has been loaded", func(t *testing.T) {
   368  
   369  		// at this point programs cache should contain data for contract A
   370  		// only because contract B has been called
   371  
   372  		viewExecA := delta.NewView(func(owner, key string) (flow.RegisterValue, error) {
   373  			require.NotEqual(t, key, environment.ContractKey("A"))
   374  			return mainView.Peek(owner, key)
   375  		})
   376  
   377  		// run a TX using contract A
   378  		procCallA := fvm.Transaction(
   379  			callTx("A", addressA),
   380  			derivedBlockData.NextTxIndexForTestingOnly())
   381  
   382  		err = vm.Run(context, procCallA, viewExecA)
   383  		require.NoError(t, err)
   384  
   385  		require.Contains(t, procCallA.Logs, "\"hello from A\"")
   386  
   387  		compareViews(t, txAView, viewExecA)
   388  
   389  		// merge it back
   390  		err = mainView.MergeView(viewExecA)
   391  		require.NoError(t, err)
   392  	})
   393  
   394  	t.Run("deploying contract C invalidates C", func(t *testing.T) {
   395  		require.NotNil(t, contractBView)
   396  
   397  		// deploy contract C
   398  		procContractC := fvm.Transaction(
   399  			contractDeployTx("C", contractCCode, addressC),
   400  			derivedBlockData.NextTxIndexForTestingOnly())
   401  		err := vm.Run(context, procContractC, mainView)
   402  		require.NoError(t, err)
   403  
   404  		entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation)
   405  		entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation)
   406  		entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation)
   407  
   408  		require.NotNil(t, entryA)
   409  		require.NotNil(t, entryB)
   410  		require.Nil(t, entryC)
   411  
   412  	})
   413  
   414  	t.Run("importing C should chain-import B and A", func(t *testing.T) {
   415  		procCallC := fvm.Transaction(
   416  			callTx("C", addressC),
   417  			derivedBlockData.NextTxIndexForTestingOnly())
   418  
   419  		viewExecC := delta.NewView(mainView.Peek)
   420  
   421  		err = vm.Run(context, procCallC, viewExecC)
   422  		require.NoError(t, err)
   423  
   424  		require.Contains(t, procCallC.Logs, "\"hello from C, hello from B but also hello from A\"")
   425  
   426  		// program A is the same
   427  		entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation)
   428  		require.NotNil(t, entryA)
   429  
   430  		require.IsType(t, entryA.State.View(), &delta.View{})
   431  		deltaA := entryA.State.View().(*delta.View)
   432  		compareViews(t, contractAView, deltaA)
   433  
   434  		// program B is the same
   435  		entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation)
   436  		require.NotNil(t, entryB)
   437  
   438  		require.IsType(t, entryB.State.View(), &delta.View{})
   439  		deltaB := entryB.State.View().(*delta.View)
   440  		compareViews(t, contractBView, deltaB)
   441  
   442  		// program C assertions
   443  		entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation)
   444  		require.NotNil(t, entryC)
   445  
   446  		// assert dependencies are correct
   447  		require.Len(t, entryC.Value.Dependencies, 3)
   448  		require.NotNil(t, entryC.Value.Dependencies[common.MustBytesToAddress(addressA.Bytes())])
   449  		require.NotNil(t, entryC.Value.Dependencies[common.MustBytesToAddress(addressB.Bytes())])
   450  		require.NotNil(t, entryC.Value.Dependencies[common.MustBytesToAddress(addressC.Bytes())])
   451  	})
   452  }
   453  
   454  // compareViews compares views using only data that matters (ie. two different hasher instances
   455  // trips the library comparison, even if actual SPoCKs are the same)
   456  func compareViews(t *testing.T, a, b *delta.View) {
   457  	require.Equal(t, a.Delta(), b.Delta())
   458  	require.Equal(t, a.Interactions(), b.Interactions())
   459  	require.Equal(t, a.ReadsCount(), b.ReadsCount())
   460  	require.Equal(t, a.SpockSecret(), b.SpockSecret())
   461  }