Turing Implementation
Learn how to use Turing hybrid compute
Turing is a system for interacting with the outside world from within solidity smart contracts. All data returned from external APIs, such as social networking or weather data are deposited into a public data-storage contract on Ethereum Mainnet. This extra data allows replicas, verifiers, and fraud-detectors to reproduce and validate the Boba L2 blockchain, block by block.
Using Turing is as easy as calling specific functions from inside your smart contract. For example, to obtain a random number for minting NFTs, call:
1
// ERC721.sol
2
random_number = turing.getRandom()
3
4
// Test/Debug Response
5
Turing NFT Random 256
6
256 bit random number as a BigInt = 61245594159531997717158776666900035572992757857563713350570408643552830626492n
7
Minted an NFT with Attribute A = 135 and Attribute B = 103
8
Minted a pirate with a green hat
9
✓ should mint an NFT with random attributes (65ms)
Copied!
To obtain Twitter or Spotify data you could set up a system like this:
1
urlStr = 'https://_myAPIURL_/social'
2
likes = social.getCurrentLikes(tweetUniqueID)
3
4
// Test/Debug response
5
Tweet 123456789 had: 18 likes by time: 1650534735
Copied!

Feature Highlight 1: Using Turing to mint an NFT with 256 random attributes in a single transaction

With Turing, your ERC721 contract can generate a cryptographically strong 256 bit random number immediately prior to the execution flow moving to the mint function. This is an atomic transaction - everything takes places within one transaction:
1
// modified mint function in a standard ERC721.sol
2
function mint(address to, uint256 tokenId) public {
3
uint256 result = myHelper.TuringRandom();
4
bytes memory i_bytes = abi.encodePacked(result);
5
uint8 attribute_1 = uint8(i_bytes[ 0]);
6
uint8 attribute_2 = uint8(i_bytes[ 1]);
7
...
8
uint8 attribute_32 = uint8(i_bytes[31]);
9
// use the attributes here to e.g. set URI/Attributes etc
10
_mint(to, tokenId);
11
emit MintedRandom(result, attribute_1, attribute_2, ...);
12
}
13
14
// pseudocode transaction response from test system (see boba/turing/test/006_NFT_random.ts)
15
256 bit random number as a BigInt = 61245594159531997717158776666900035572992757857563713350570408643552830626492n
16
Minted an NFT with Attribute A = 135 and Attribute B = 103
17
Minted a pirate with a green hat
18
✓ should mint an NFT with random attributes (65ms)
Copied!
To use this functionality, deploy your TuringHelper contract, provide its address to your ERC721 contract, and make the TuringHelper aware of the new caller:
1
// deploy your Turing helper
2
myTuringHelper = await Factory__Helper.deploy()
3
4
// deploy your ERC721 contract with the
5
erc721 = await Factory__ERC721.deploy("RandomERC721", "RER", myTuringHelper.address)
6
7
// restrict your myHelper to accept only requests from your ERC721
8
await myTuringHelper.addPermittedCaller(erc721.address)
Copied!
Then, register and fund your Turing Credit account:
1
const ONE_BOBA = utils.parseEther('1')
2
await turingCredit.addBalanceTo(ONE_BOBA, myTuringHelper.address)
Copied!
All done! Each Turing request costs 0.01 BOBA, so 1 BOBA is enough for 100 Turing requests. Have fun. You can find example code and an ERC721 that uses Turing here and a fully-featured Turing-ready NFT system here.

Feature Highlight 2: Using Turing to access APIs from within your solidity smart contract

You can use Turing as a pipe to any other computer, such as APIs for social networks, weather and location data, or market data. Please keep in mind however that Turing differs sharply from established providers of market trading data, in particular, since Turing does not provide a decentralized mechanism to verify the accuracy of the data. You should therefore not use Turing for production trading or lending use, but should use proven, decentralized data oracles.
Data/Oracle best practices Regardless of your specific use case, minimally, you will need to secure your pipe/contract against data outliers, temporary lack of data, and malicious attempts to distort the data. For example, you could average over multiple on-chain oracles and/or off-chain sources - in this case, the role of Turing could be to 'augment' or separately estimate the reliability and timeliness of on-chain oracles.
Note - Boba does not provide endpoints for you You are responsible for setting up an endpoint that Turing can access - read on for more information and example code. Assume you have an API access key to a provider of weather data. First, set up a server or endpoint that queries this API, and stores and analyzes the data, if needed. Your own server/endpoint contains your secrets and API access keys. Next, add a simple interface to allow Turing to interact with your server. Turing calls to your server contain the address of the calling contract and there are multiple ways to control access to your server in very granular manner, if desired. See .packages/boba/turing/AWS_code/turing_oracle.py for a copy-paste example for querying data APIs via a wrapper at AWS Lambda:
1
/AWS_code/turing_oracle.py
2
3
# Note - This code is running on YOUR server
4
5
...
6
api_key = 'YOUR_API_KEY' # Insert your API key here
7
8
authorized_contract = None # for open access
9
# or...
10
authorized_contract = '0xOF_YOUR_HELPER_CONTRACT' # to restrict access to only your smart contract
11
...
Copied!
You should lock down your off-chain endpoint to only accept queries from your smart contract. To do this, designate your smart contract's address on Boba as the authorized_contract. If you wish to allow open access, set this variable to None. You can then call this API in your smart contract:
1
urlStr = 'https://_myAPIURL_/social'
2
likes = social.getCurrentLikes(tweetUniqueID)
3
4
// Test/Debug response
5
Tweet 123456789 had: 18 likes by time: 1650534735
Copied!

AWS and Google Cloud Function Examples

Your external API will need to accept calls from the L2Geth and return data in a way that can be understood by the L2Geth. Examples are provided in ./packages/boba/turing/AWS_code. Specific instructions for setting up AWS lambda endpoints are here - note that all APIs can be used, not just AWS Lambda endpoints.

Important Properties of Turing

  • Strings returned from external endpoints are limited to 322 characters (5*64+2=322)
  • Only one Turing call per execution
  • There is 1200 ms timeout on API responses. Please make sure that your API responds promptly. If you are using AWS, note that some of their services take several seconds to spin up from a 'coldstart', resulting in persistent failure of your first call to your endpoint.

String length limit

The string length cap of 322 is large enough to return, for example, four uint256 from the external api:
1
//example: returing 4 unit264
2
3
// 0x
4
// 0000000000000000000000000000000000000000000000000000000000000080 ** length of the dynamic bytes
5
// 0000000000000000000000000000000000000000000000000000000000418b95 ** first uint256
6
// 0000000000000000000000000000000000000000000000000000017e60d3b45f **
7
// 0000000000000000000000000000000000000000000000000000000000eb7ca3 **
8
// 00000000000000000000000000000000000000000000000000000000004c788f ** fourth unit265
Copied!
You can return anything you want - e.g. numbers, strings, ... - and this information will then later be decoded per your abi.decode. For example, if the external API sends two unit256:
1
// Payload from the external API
2
// 0x
3
// 0000000000000000000000000000000000000000000000000000000000000040 ** length of the dynamic bytes
4
// 0000000000000000000000000000000000000000000000000000000000418b95 ** first uint256
5
// 0000000000000000000000000000000000000000000000000000017e60d3b45f ** second uint256
6
7
// decoding of those data within the smart contract
8
(uint256 market_price, uint256 time) = abi.decode(encResponse,(uint256,uint256));
Copied!

One Turing call per Transaction

At present, you can only have one Turing call per transaction, i.e. a Turing call cannot call other contracts that invoke Turing as well. Transactions that result in multiple Turing calls in the call stack will revert.

Turing Architecture

The modified Turing L2Geth, L2TGeth, monitors calldata for particular Keccak methodIDs of functions such as GetRandom(uint32 rType, uint256 _random) and GetResponse(uint32 rType, string memory _url, bytes memory _payload). Upon finding such methodIDs in the execution flow, at any level, L2TGeth parses the calldata for additional information, such as external URLs, and uses that information to either directly prepare a response (e.g. generate a random number) or to call an external API. After new information is generated (or has returned from the external API), L2TGeth then runs the function with updated inputs, such that the new information flows back to the caller (via overloaded variables and a system for conditionally bypassing requires). Put simply, L2TGeth intercepts function calls, adds new information to the inputs, and then runs the function with the updated inputs.
In general, this system would lead to disagreement about the correct state of the underlying blockchain. For example, if replicas and verifiers simply ingested the transactions and re-executed them, then every blockchain would differ, destroying the entire system. Thus, a new data field called Turing (aka turing, l1Turing or L1Turing depending on context) has been added to the L2Geth transactions,messages, receipts, blocks, evm.contexts, and various codecs and encoders/decoders. This new data field is understood by core-utils as well as the data-translation-layer and the batch-submitter, and allows Turing data to be pushed into, and recovered from, the CanonicalTransactionChain (CTC). This extra information allows all verifiers and replicas to enter a new replay mode, where instead of generating new random numbers (or calling off-chain for new data), they use the Turing data stored in the CTC (or in the L2 blocks as part of the transaction metadata) to generate a faithful copy of the main Boba L2 blockchain. Thus, the overall system works as before, with all the information needed for restoring the Boba L2 and, just as critically, for public fraud detection, being publicly deposited into Ethereum.

Quickstart for Turing Developers

Open a terminal window and from the top level:
1
$ yarn
2
$ yarn build
3
$ cd ops
4
$ BUILD=1 DAEMON=0 ./up_local.sh
Copied!
This will spin up the stack. Then, open a second terminal window and:
1
$ cd packages/boba/turing
2
$ yarn test:local
Copied!
Note: Testing on Rinkeby
To test on Rinkeby, you need a private key with both ETH and BOBA on the Boba L2; the private key needs to be provided in hardhat.config.js. Just replace all the zeros with your key:
1
boba_rinkeby: {
2
url: 'https://rinkeby.boba.network',
3
accounts: ['0x0000000000000000000000000000000000000000000000000000000000000000']
4
},
Copied!
Then, run:
1
$ cd packages/boba/turing
2
$ yarn test:rinkeby
Copied!
The tests will perform some basic floating point math, provide some random numbers, and get the latest BTC-USD exchange rate:
1
yarn run v1.22.15
2
$ hardhat --network boba_local test
3
4
Stableswap at AWS Lambda
5
URL set to https://i9iznmo33e.execute-api.us-east-1.amazonaws.com/swapy
6
Helper contract deployed as 0x8e264821AFa98DD104eEcfcfa7FD9f8D8B320adA
7
Stableswap contract deployed as 0x871ACbEabBaf8Bed65c22ba7132beCFaBf8c27B5
8
addingPermittedCaller to TuringHelper 0x000000000000000000000000871acbeabbaf8bed65c22ba7132becfabf8c27b5
9
Test contract whitelisted in TuringHelper (1 = yes)? 1
10
✓ contract should be whitelisted (50ms)
11
Credit Prebalance 0
12
BOBA Balance in your account 300000000000000000000
13
✓ Should register and fund your Turing helper contract in turingCredit (172ms)
14
✓ should return the helper address (116ms)
15
result of x_in 12 -> y_out = 50
16
✓ should correctly swap X in for Y out (202ms)
17
18
Turing 256 Bit Random Number
19
Helper contract deployed at 0xb185E9f6531BA9877741022C92CE858cDCc5760E
20
Test contract deployed at 0xAe120F0df055428E45b264E7794A18c54a2a3fAF
21
addingPermittedCaller to TuringHelper 0x000000000000000000000000ae120f0df055428e45b264e7794a18c54a2a3faf
22
Test contract whitelisted in TuringHelper (1 = yes)? 1
23
✓ contract should be whitelisted (51ms)
24
Credit Prebalance 0
25
BOBA Balance in your account 290000000000000000000
26
✓ Should register and fund your Turing helper contract in turingCredit (174ms)
27
Turing 42 = 42
28
✓ should get the number 42 (91ms)
29
Turing VRF 256 = 11642062518220346831211086370276871135010213271872466428492348202384902597141n
30
✓ should get a 256 bit random number (83ms)
31
Turing VRF 256 = 39492154036951735205025381980653780356965271743173916331971607322325246415525n
32
✓ should get a 256 bit random number (83ms)
33
34
Pull Bitcoin - USD quote
35
URL set to https://i9iznmo33e.execute-api.us-east-1.amazonaws.com/quote
36
Helper contract deployed as 0x7C8BaafA542c57fF9B2B90612bf8aB9E86e22C09
37
Lending contract deployed as 0x0a17FabeA4633ce714F1Fa4a2dcA62C3bAc4758d
38
addingPermittedCaller to TuringHelper 0x0000000000000000000000000a17fabea4633ce714f1fa4a2dca62c3bac4758d
39
Test contract whitelisted in TuringHelper (1 = yes)? 1
40
✓ contract should be whitelisted (53ms)
41
✓ should return the helper address
42
Credit Prebalance 0
43
BOBA Balance in your account 280000000000000000000
44
✓ Should register and fund your Turing helper contract in turingCredit (176ms)
45
Bitcoin to USD price is 36654.89
46
timestamp 1643158948154
47
✓ should get the current Bitcoin - USD price (305ms)
48
49
Turing NFT Random 256
50
Turing Helper contract deployed at 0xd9fEc8238711935D6c8d79Bef2B9546ef23FC046
51
ERC721 contract deployed at 0xd3FFD73C53F139cEBB80b6A524bE280955b3f4db
52
adding your ERC721 as PermittedCaller to TuringHelper 0x000000000000000000000000d3ffd73c53f139cebb80b6a524be280955b3f4db
53
Credit Prebalance 0
54
BOBA Balance in your account 270000000000000000000
55
✓ Should register and fund your Turing helper contract in turingCredit (122ms)
56
ERC721 contract whitelisted in TuringHelper (1 = yes)? 1
57
✓ Your ERC721 contract should be whitelisted
58
256 bit random number as a BigInt = 61245594159531997717158776666900035572992757857563713350570408643552830626492n
59
Minted an NFT with Attribute A = 135 and Attribute B = 103
60
Minted a pirate with a green hat
61
✓ should mint an NFT with random attributes (65ms)
62
63
64
22 passing (3s)
65
66
✨ Done in 6.67s.
Copied!

Technical Appendix: Implementation Details

Step 1: Invoking Turing for inside a Smart contract

A Turing cycle starts with specific function calls inside solidity smart contracts deployed on Boba:
1
random_number = turing.getRandom()
Copied!
The modified L2TGeth detects these function calls, intercepts them, and obtains requested data from other sources (strong random number generators, off-chain APIs and datafeeds, ...).
1
/l2geth/core/vm/evm.go
2
3
// Call executes the contract associated with the addr with the given input as
4
// parameters. It also handles any necessary value transfer required and takes
5
// the necessary steps to create accounts and reverses the state in case of an
6
// execution error or failed value transfer.
7
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
8
9
...
10
11
//methodID for GetResponse is 7d93616c -> [125 147 97 108]
12
isTuring2 := bytes.Equal(input[:4], []byte{125, 147, 97, 108})
13
14
//methodID for GetRandom is 493d57d6 -> [73 61 87 214]
15
isGetRand2 := bytes.Equal(input[:4], []byte{73, 61, 87, 214})
16
17
// TuringCall takes the original calldata, figures out what needs
18
// to be done, and then synthesizes a 'updated_input' calldata
19
var updated_input hexutil.Bytes
20
21
if isTuring2 {
22
if len(evm.Context.Turing) < 3 {
23
// This is the first run of Turing for this transaction
24
// We sometimes use a short evm.Context.Turing payload for debug purposes.
25
// A real modified callData is always much much > 2 bytes
26
// This case _should_ never happen in Verifier/Replica mode, since the sequencer will already have run the Turing call
27
updated_input = bobaTuringCall(input, caller.Address())
28
ret, err = run(evm, contract, updated_input, false)
29
// and now, provide the updated_input to the context so that the data can be sent to L1 and the CTC
30
/**************** CRITICAL LINE ****************/
31
evm.Context.Turing = updated_input
32
/**************** CRITICAL LINE ****************/
33
} else {
34
// Turing for this Transaction has already been run elsewhere - replay using
35
// information from the EVM context
36
ret, err = run(evm, contract, evm.Context.Turing, false)
37
}
38
} else if isGetRand2 {
39
if len(evm.Context.Turing) < 3 {
40
// See above - they apply 1:1 here too
41
updated_input = bobaTuringRandom(input)
42
ret, err = run(evm, contract, updated_input, false)
43
44
/**************** CRITICAL LINE ****************/
45
evm.Context.Turing = updated_input
46
/**************** CRITICAL LINE ****************/
47
} else {
48
// Turing for this Transaction has already been run elsewhere - replay using
49
// information from the EVM context
50
ret, err = run(evm, contract, evm.Context.Turing, false)
51
}
52
} else {
53
ret, err = run(evm, contract, input, false)
54
}
55
56
...
Copied!
The random number generation is done locally, inside the Geth, and off-chain APIs are queried with standard calls:
1
/l2geth/core/vm/evm.go
2
3
// In response to an off-chain Turing request, obtain the requested data and
4
// rewrite the parameters so that the contract can be called without reverting.
5
func bobaTuringRandom(input []byte) hexutil.Bytes {
6
7
var ret hexutil.Bytes
8
9
rest := input[4:]
10
11
//some things are easier with a hex string
12
inputHexUtil := hexutil.Bytes(input)
13
14
// If things fail, we'll return an integer parameter which will fail a
15
// "require" in the contract.
16
retError := make([]byte, len(inputHexUtil))
17
copy(retError, inputHexUtil)
18
19
// Check the rType
20
// 1 for Request, 2 for Response, integer >= 10 for various failures
21
rType := int(rest[31])
22
if rType != 1 {
23
log.Warn("TURING-1 bobaTuringRandom:Wrong state (rType != 1)", "rType", rType)
24
retError[35] = 10 // Wrong input state
25
return retError
26
}
27
28
rlen := len(rest)
29
if rlen < 2*32 {
30
log.Warn("TURING-2 bobaTuringRandom:Calldata too short", "len < 2*32", rlen)
31
retError[35] = 11 // Calldata too short
32
return retError
33
}
34
35
// Generate cryptographically strong pseudo-random int between 0 - 2^256 - 1
36
one := big.NewInt(1)
37
two := big.NewInt(2)
38
max := new(big.Int)
39
// Max random value 2^256 - 1
40
max = max.Exp(two, big.NewInt(int64(256)), nil).Sub(max, one)
41
n, err := rand.Int(rand.Reader, max)
42
43
if err != nil {
44
log.Warn("TURING bobaTuringRandom: Random Number Generation Failed", "err", err)
45
retError[35] = 16 // RNG Failure
46
return retError
47
}
48
49
//generate a BigInt random number
50
randomBigInt := n
51
52
// build the calldata
53
methodID := make([]byte, 4)
54
copy(methodID, inputHexUtil[0:4])
55
ret = append(methodID, hexutil.MustDecode(fmt.Sprintf("0x%064x", 2))...) // the usual prefix and the rType, now changed to 2
56
ret = append(ret, hexutil.MustDecode(fmt.Sprintf("0x%064x", randomBigInt))...)
57
58
return ret
59
}
60
61
// In response to an off-chain Turing request, obtain the requested data and
62
// rewrite the parameters so that the contract can be called without reverting.
63
func bobaTuringCall(input []byte, caller common.Address) hexutil.Bytes {
64
65
var responseStringEnc string
66
var responseString []byte
67
68
rest := input[4:]
69
inputHexUtil := hexutil.Bytes(input)
70
restHexUtil := inputHexUtil[4:]
71
72
retError := make([]byte, len(inputHexUtil))
73
copy(retError, inputHexUtil)
74
75
// Check the rType
76
// 1 for Request, 2 for Response, integer >= 10 for various failures
77
rType := int(rest[31])
78
if rType != 1 {
79
retError[35] = 10 // Wrong input state
80
return retError
81
}
82
83
rlen := len(rest)
84
if rlen < 7*32 {
85
retError[35] = 11 // Calldata too short
86
return retError
87
}
88
89
// A micro-ABI decoder... this works because we know that all these numbers can never exceed 256
90
// Since the rType is 32 bytes and the three headers are 32 bytes each, the max possible value
91
// of any of these numbers is 32 + 32 + 32 + 32 + 64 = 192
92
// Thus, we only need to read one byte
93
94
// 0 - 31 = rType
95
// 32 - 63 = URL start
96
// 64 - 95 = payload start
97
// 96 - 127 = length URL string
98
// 128 - ??? = URL string
99
// ??? - ??? = payload length
100
// ??? - end = payload
101
102
startIDXurl := int(rest[63]) + 32
103
// the +32 means that we are going directly for the actual string
104
// bytes 0 to 31 are the string length
105
106
startIDXpayload := int(rest[95]) // the start of the payload
107
lengthURL := int(rest[127]) // the length of the URL string
108
109
// Check the URL length
110
// Note: we do not handle URLs that are longer than 64 characters
111
if lengthURL > 64 {
112
retError[35] = 12 // URL string > 64 bytes
113
return retError
114
}
115
116
// The URL we are going to query
117
endIDX := startIDXurl + lengthURL
118
url := string(rest[startIDXurl:endIDX])
119
// we use a specific end value (startIDXurl+lengthURL) since the URL is right-packed with zeros
120
121
// At this point, we have the API endpoint and the payload that needs to go there...
122
payload := restHexUtil[startIDXpayload:] //using hex here since that makes it easy to get the string
123
124
log.Debug("TURING-4 bobaTuringCall:Have URL and payload",
125
"url", url,
126
"payload", payload)
127
128
client, err := rpc.Dial(url)
129
130
if client != nil {
131
if err := client.Call(&responseStringEnc, caller.String(), payload); err != nil {
132
retError[35] = 13 // Client Error
133
return retError
134
}
135
responseString, err = hexutil.Decode(responseStringEnc)
136
if err != nil {
137
retError[35] = 14 // Client Response Decode Error
138
return retError
139
}
140
} else {
141
retError[35] = 15 // Could not create client
142
return retError
143
}
144
145
// build the modified calldata
146
ret := make([]byte, startIDXpayload+4)
147
copy(ret, inputHexUtil[0:startIDXpayload+4]) // take the original input
148
ret[35] = 2 // change byte 3 + 32 = 35 (rType) to indicate a valid response
149
ret = append(ret, responseString...) // and tack on the payload
150
151
return ret
152
}
Copied!

Step 2: Flow of Turing data out of the evm.context

l2geth/core/state_processor.go:core.ApplyTransaction moves the Turing data from Context.Turing into the transaction.meta.L1Turing byte array:
1
l2geth/core/state_processor.go
2
3
// ApplyTransaction attempts to apply a transaction to the given state database
4
// and uses the input parameters for its environment. It returns the receipt
5
// for the transaction, gas used and an error if the transaction failed,
6
// indicating the block was invalid.
7
func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, error) {
8
...
9
109 // Apply the transaction to the current state (included in the env)
10
110 _, gas, failed, err := ApplyMessage(vmenv, msg, gp)
11
111: // TURING Update the tx metadata, if a Turing call took place...
12
112 if len(vmenv.Context.Turing) > 1 {
13
113 tx.SetL1Turing(vmenv.Context.Turing)
14
114 }
Copied!
The Turing data are subsequently incorporated into new L2 blocks via w.engine.FinalizeAndAssemble - the Turing data are in the w.current.txs input.
1
l2geth/miner/worker.go:
2
3
// commit runs any post-transaction state modifications, assembles the final block
4
// and commits new work if consensus engine is running.
5
func (w *worker) commit(uncles []*types.Header, interval func(), start time.Time) error {
6
...
7
1110 s := w.current.state.Copy()
8
1111 // log.Debug("TURING worker.go final block", "depositing_txs", w.current.txs)
9
1112: block, err := w.engine.FinalizeAndAssemble(w.chain, w.current.header, s, w.current.txs, uncles, w.current.receipts)
10
1113 if err != nil {
11
1114 return err
Copied!
At this point, the data are circulated to various places throughout the system as part of the block/transaction data. Notably, calls to the L2 for block/transaction data now return a new field, l1Turing to all callers.

Step 3: Batch submitter Turing data injection

The batch submitter receives an new block/transaction from L2TGeth, obtains the raw call string (rawTransaction) and the Turing data (l1Turing), and if there was a Turing event (as judged from the length of the Turing string), the modified batch-submitter appends those data to the rawTransaction string. From the perspective of the CTC, it is receiving its normal batch payload.
1
// batch-submitter tx-batch-submitter.ts
2
3
private async _getL2BatchElement(blockNumber: number): Promise<BatchElement> {
4
5
// Idea - manipulate the rawTransaction as early as possible, so we do not have to change even more of the encode/decode
6
// logic - note that this is basically adding a second encoder/decoder before the 'normal' one, which encodes total length
7
//
8
// The 'normal' one will now specify the TOTAL length (new_turing_header + rawTransaction + turing (if != 0)) rather than
9
// just remove0x(rawTransaction).length / 2
10
11
...
12
13
if (this._isSequencerTx(block)) {
14
batchElement.isSequencerTx = true
15
const turing = block.transactions[0].l1Turing
16
let rawTransaction = block.transactions[0].rawTransaction
17
if (turing.length > 4) {
18
// FYI - we sometimes use short (length <= 4) non-zero Turing strings for debug purposes
19
// Chop those off at this stage
20
// Only propagate the data through the system if it's a real Turing payload
21
const headerTuringLengthField = remove0x(BigNumber.from(remove0x(turing).length / 2).toHexString()).padStart(6, '0')
22
rawTransaction = '0x' + headerTuringLengthField + remove0x(rawTransaction) + remove0x(turing)
23
} else {
24
rawTransaction = '0x' + '000000' + remove0x(rawTransaction)
25
}
26
batchElement.rawTransaction = rawTransaction
27
}
Copied!

Step 4: Writing to the CTC

The batch-submitter writes the data to the CTC as usual. The CTC does not know about Turing - that was one of the goals, so we do not have to modify the L1 contracts.

Step 5: DTL Turing data extraction; Reading from the CTC

The DTL reads from the CTC and unpacks the modified rawTransaction (which is now called sequencerTransaction). The DTL uses a Turing length metadata field in the sequencerTransaction string. Critically, the DTL writes a slightly modified TransactionEntry into its database, which has a new field called turing. When the database is queried, it thus returns the Turing data in addition to all the usual fields.
1
// DTL services/l1-ingestion/handles/sequencer-batch-appended.ts
2
3
for (let j = 0; j < context.numSequencedTransactions; j++) {
4
...
5
6
// need to keep track of the original length so the pointer system for accessing
7
// the individual transactions works correctly
8
const sequencerTransaction_original_length = sequencerTransaction.length
9
10
// This MIGHT have a Turing payload inside of it...
11
// First, parse the new length field...
12
const sTxHexString = toHexString(sequencerTransaction)
13
const turingLength = parseInt(remove0x(sTxHexString).slice(0,6), 16)
14
15
let turing = Buffer.from('0')
16
17
if (turingLength > 0) {
18
//we have Turing payload
19
turing = sequencerTransaction.slice(-turingLength)
20
sequencerTransaction = sequencerTransaction.slice(3, -turingLength)
21
// The `3` chops off the Turing length header field, and the `-turingLength` chops off the Turing bytes
22
console.log('Found a Turing payload at (neg) position:', {
23
turingLength,
24
turing: toHexString(turing),
25
restoredSequencerTransaction: toHexString(sequencerTransaction),
26
})
27
} else {
28
// The `3` chops off the Turing length header field, which is zero in this case (0: 00 1: 00 2: 00)
29
sequencerTransaction = sequencerTransaction.slice(3)
30
}
31
32
transactionEntries.push({
33
...
34
turing: toHexString(turing),
35
})
Copied!

Step 6: Verifier data ingestion

The Verifier receives all the usual data from the DTL, but, if there was a Turing call, there is now an additional data field containing the rewritten callData as a HexString. The Turing data are obtained from incoming json data and are written into the transaction metadata, meta.L1Turing = turing:
1
/l2geth/core/types/transaction_meta.go:
2
38 L1Timestamp uint64 `json:"l1Timestamp"`
3
39: L1Turing []byte `json:"l1Turing" gencodec:"required"`
4
40 L1MessageSender *common.Address `json:"l1MessageSender" gencodec:"required"`
5
..
6
55 l1Timestamp uint64,
7
56: l1Turing []byte,
8
57 l1MessageSender *common.Address,
9
..
10
64 L1Timestamp: l1Timestamp,
11
65: L1Turing: l1Turing,
12
66 L1MessageSender: l1MessageSender,
13
..
14
145 }
15
146
16
147: turing, err := common.ReadVarBytes(b, 0, 2048, "Turing")
17
148 if err != nil {
18
149 return nil, err
19
150 }
20
151: if !isNullValue(turing) {
21
152: meta.L1Turing = turing
22
153 }
Copied!
At this point, the Turing data can be passed into the evm.context, which then triggers the else logic in the evm.go:
1
/l2geth/core/vm/evm.go
2
3
// Call executes the contract associated with the addr with the given input as
4
// parameters. It also handles any necessary value transfer required and takes
5
// the necessary steps to create accounts and reverses the state in case of an
6
// execution error or failed value transfer.
7
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
8
9
...
10
11
//methodID for GetResponse is 7d93616c -> [125 147 97 108]
12
isTuring2 := bytes.Equal(input[:4], []byte{125, 147, 97, 108})
13
14
//methodID for GetRandom is 493d57d6 -> [73 61 87 214]
15
isGetRand2 := bytes.Equal(input[:4], []byte{73, 61, 87, 214})
16
17
// TuringCall takes the original calldata, figures out what needs
18
// to be done, and then synthesizes a 'updated_input' calldata
19
var updated_input hexutil.Bytes
20
21
if isTuring2 {
22
if len(evm.Context.Turing) < 3 {
23
...
24
} else {
25
// Turing for this Transaction has already been run elsewhere - replay using
26
// information from the EVM context
27
ret, err = run(evm, contract, evm.Context.Turing, false)
28
}
29
} else if isGetRand2 {
30
if len(evm.Context.Turing) < 3 {
31
...
32
} else {
33
// Turing for this Transaction has already been run elsewhere - replay using
34
// information from the EVM context
35
ret, err = run(evm, contract, evm.Context.Turing, false)
36
}
37
} else {
38
ret, err = run(evm, contract, input, false)
39
}
40
41
...
Copied!
The Turing data flow from out from the evm.context through the rest of the system as before, so the data are incorporated into verifier and replica blocks, resulting in correct/consistent state roots and replica and verifier blocks.