NEAR Live Contract Review | Episode 10: Rainbow Bridge ๐
Nothing is loaded from YouTube until you press play, and then without tracking cookies.
About this video
We discuss the Rainbow Bridge. It contains basic explanation how the Rainbow bridge works and the two contracts written in Rust.
The first contract is Ethereum light-client. It keeps track of the canonical Ethereum blockchain by using block headers and verifying proof of work. At this contract we've discovered a critical security vulnerability that was already patched in the master branch and the prod contracts were upgraded. (This is the reason why this episode was delayed).
The second contract is the prover. It proves that the given event was present in the Ethereum block by verifying merkle path towards the root from the block header.
Source code: github.com/near/rainbow-bridge
Follow the latest from NEAR Protocol on,
Website: nearprotocol.com Discord: near.ai/discord Medium: near.ai/medium Twitter: near.ai/twitter GitHub: near.ai/github
#WhiteboardSeries #Blockchain #FutureIsNEAR
Transcript
5,514 wordsCaptions as YouTube publishes them, usually machine-generated, so product and people names can be misheard.
- 0:02
hello everyone this is another episode of weekly live contracts and today we're gonna discuss uh rainbow bridge especially the contracts on the near side and the way we verify ethereum blocks and also take a look at the prover which is how we prove that some event exists on ethereum site using proof from the client on your site okay uh let me present cool so this is um rainbow bridge
- 1:02
and it's split into contracts you have east and near east ethereum it's a ethereum contracts near the contracts on near side and it's split into multiple contracts so one of them ethereum client so this one proves uh this one reconstructs chain using light like block headers from ethereum and it verifies proof of work to find the canonical chain next is isprover his prover is a contract that takes a proof a block
- 1:52
height and verifies that this block is present and it's in it's a part of the canonical chain and these types is a utility that shared both by his client and isprover there's also mutable fungible token but we'll probably not have enough time today to review this but it allows to mean tokens when they are locked on ethereum site so let's actually take a look at uh what's going on on uh ethereum client first
- 2:39
so if you haven't done this contract yeah go ahead i'm i'm just curious from like a really high level so this is like eth client and there's probably a near client that's like a light client like how how would you quickly explain like what is like like client yes good question so light client is um some kind of program that tracks a blockchain so it allows you to verify that a given block is part of the blockchain and it's part of the
- 3:16
canonical blockchain so for proof of work system it actually verifies the proof of work which is fairly simple you just need to compute the hash of the header that that is given to you and then verify that this hash is um has enough zeros in front of it and this is the power of work and then on ethereum for near light client you need to verify signatures and reconstruct the chain using um
- 3:57
a proper transition so basically there's fastpass that allows you to skip entire e-books i assume and there's a short pause that allows you to skip from the beginning of the e-book to the given header and all you need to do is to know who validators are from the previous e-books and so who are active validators for the given e-book and then verify their signatures to make sure
- 4:30
um that the signature matches on the header that the way you prove that enough stake validated this given transition so we cannot really do this on ethereum because it costs too much gas right now so instead we rely on challenges so there is a that's a optimistic approach so we post a block then the block is backed by enough stake then anyone can challenge this block by providing an invalid proof like
- 5:08
indicating which signature is invalid and if you prove it then you keep the stake and the block is rewarded so the plan is to search for pessimistic bridge or like actual real bridge where all signatures are verified and we may need to switch our curve for validation and provide some extra records in a block so you can actually validate it on every header so you don't need to do challenges that will allow you to
- 5:43
have like super fast finality on the theory so you don't need to wait for challenges and i don't need to care about challenges does that make sense cool so the client is essentially keeps track of previous canonical black block header hashes so they stored in a map by height in this case block number from ethereum towards the hash of the block we use types from ethereum they slightly re-implemented to implement board serialization so
- 6:42
now if you wanna know what's the block header at the given height you can query it we look at the canonical header hashes at the given height we find the block block header cache then you can look up the block header hash and get the block header there's some extra stuff that needed to upgrade and there are some initialized stuff that i needed to verify the is hash so east was designed to be somewhat asic resistant so they designed uh some
- 7:41
dag that you need to create which takes uh space and we cannot really store two gigs of daggers like directed a cyclical graph right yes i i don't know exact details of how like if ethereum is constructed or what it is i just know that occasionally you need to look up some at some place in this dag it's kind of like a memory aspect of proof of work and use this value to compute
- 8:31
and for anyone listening in who hasn't heard of a dag or a directed acyclic graph my mental model is it's like a javascript object but none of the keys anywhere within it can like reference something higher up in it that would be cyclic so it's acyclic right yeah so throw that in there for for any newbies cool the main point why we need it is to to be able to compute this hash
- 9:09
for a given ethereum epoch efficiently so instead of um instead of actually having this uh object we have something like 5 12 like 512 pre-computed merkle roots of the dax that we use then we can prove that at this position in in this offset the value is present given a proof to the merkle root so this is computed once and and given to um to the contract at the initialization time
- 10:12
now if you need to compute it hash for a given epoch you also provide a proof that the given epoch at this position there is a certain value then if you go up on the merkle tree and verify like compute the caches you should reach um a route that we have here and that's how merkle proofs and merkle trees uh work and concept of the merkle proof is very widely used in the blockchains because it allows you to compress set of information
- 11:00
without having an entire state and a prove that this set of inter information is present in the try at a given position if you only need to provide a logarithmic number of records so you need to like just go up the tree until you reach the root and the way you do this is you have for every node in a try you provide a missing value so let's say you started from some position then
- 11:38
you need to know what was the missing pi pair on the right a hash of this missing pair on the right then you can compute you can compute a parent cache by concatenating them and caching again then you need to provide another missing sibling cache and so on until you reach the root and the reason why this cannot be faked is because because of entropy of the hashes it's very low probability to have a
- 12:22
collision which is somewhat unrealistic to find in our lifetime without having quantum computers so that's why we consider merkle proofs secure at the same time ethereum uses h128 which is somewhat lower entropy and potentially eventually someone will find it with just existing computers then 256 but when it happens i will see maybe like a one collision which is not really gonna break any proof of work consent consensus uh
- 13:10
and if you want to try to find a match for a given object not just like a single collision then it's even harder because now you need to your uh size of search increases anyway so this is a background for how some of this works let's look at the contract itself so here's the initialization and the first thing you would notice that you haven't seen before probably is serializer boards so what a serializer board
- 13:46
it basically feature veneer sdk to say that this um this argument has to be serialized with borsch but because you cannot really mix json and borsch in a single set of arguments they all need to be the same default serializer is json so you need to mark every single argument with serializer borsch right now in order for this to work so what happens is in it instead of taking a json record
- 14:26
it will take it kind of constructs a structure of all these arguments as a fields of the structure and then it deserializes the structure using bors why is this important like why not use json for the initializer it seems like it would save some hassle and it's only a one-time call so is it worth optimizing i assume it's a first header that is like fairly large and so it would be potentially beyond the uh
- 15:04
you would be able to use base64 for example for this one but you still like if you you have to support borsch anyway so like it's not a big deal if you do this for one argument or for multiple plus this is more efficient and quick i understand that probably not an issue the gas that you're using is way less than the like time it takes but for optimizing one like init seems like the
- 15:34
least useful thing to optimize to me given that i assume literally once this entire i assume this entire contract is using boards for everything so that's why so it's just easier to be consistent okay right so it's kind of like a borscht country and borsch is our serializer and this is a binary serializer that is super efficient and fast but it requires a schema to serialize the sterilize
- 16:08
so it checks that it's not initialized and instead it calls a static variable so this is on the first time we actually see a static call which is kind of cool we know that it now works without having to look into um so static does not deserialize the state it does not require for state to exist but you can access uh objects internally so potentially static can be used for migrations or it can be used for something else like maybe you only need
- 16:57
to access a single element like um from an ordered map so you don't really need to deserialize the state if you can reconstruct the prefix for an ordered map what makes it slightly more efficient it doesn't have uh underscore self underscore view itself cool so you can see another bore serializer so this is the result um so result serializer tells you what uh type the object will be
- 17:39
and in this case it uh or like which serializer to use when you serialize the type so in this case serializer is more like a deserializer so but to be consistent it always use serializer so you can call this object and it will return something like one or zero in bytes a single byte that is either zero one not a character but actual value all right so let's go back to the initialization so first thing we do we use lpd code so this is coming from a
- 18:25
library specifically written by this contract but it's just a standard ethereum uh client library i assume it's written by parity and it allows to visualize serialize objects so in this case it implements what is this serialized it's a certain interesting um okay is okay uh there's probably impul rlp encodable and rlp decodable for block headers and this is how it's deserialized from
- 19:23
rlp encoding rlp encoding i think mike can give you a quick explanation with this rlp yeah i mean i just know it's recursive length prefix um but it's also similar to borscht where you do need wait a second do you need do you need a schema for rlp too no i think rlp is descriptive okay but yeah you you used in uh using ethereum for basically making binary stuff yeah um so yeah serialization uh prefix i guess actually the
- 20:09
i think that the prefix part of it might be i guess the self-describing part where now i'm remembering it's like if the thing is an array then it's gonna like start with a prefix of like you know zero three or something that that kind of deal so i can tell right how long like like what the next thing it is in here and how many bytes to look forward before it is has finished loading that very next
- 20:32
thing cool so yeah so basically the bridge has to the british clan has to get the first header from which it starts it has to be a canonical header and this is a trusted thing that you probably should verify when you use a bridge you need to check that the original header is legit and it has to be part of the canonical ethereum blockchain next you get the hash out of the header
- 21:13
number and you construct a structure which you use um to initialize so best header hash indicates the last best header that you know it's the highest proof of work header that is valid and the highest means it has the maximum proof of work uh sum so the proof of work is like a sum of all work in the past that is put into the link click like into the list linked list so every next block reference the previous block on which it
- 22:09
built and uses the proof of work of the previous block to to create the new book now some threshold for garbage collection given is is how far we need to do the garbage collection because we don't store full headers after a certain amount of time otherwise we'll grow too fast in terms of storage that the bridge consumes so we find uh we collect all the arrays headers after some period from the head of the blockchain well i guess
- 23:03
tail and it doesn't matter and so basically just keep for example last 30 thousand headers if you need to prove something from the past for the header that was garbage collected you have to provide this header potentially or you can use this header when you give it to um to the prover so you give the header we it can compute the hash of the header and it will verify just the hash of the header in this
- 23:48
chain now uh finalized gc so we say the chain is finalized the canonical chain is finalized and you cannot fork it from this given moment after this threshold is reached this one will garbage collect headers from the chain let's get it so this is for the client like the client is calculating the like what counts as final am i understanding that correctly it seems like something that like i understand the bridge fairly well already so for contacts for other people like you have other kinds of connectors for sending
- 24:37
punchable tokens or non-functional tokens and to me i would think that we would want the what counts as final in ethereum to live in different kinds of connectors for nfts you might say like if there are 10 blocks on top of it that's good enough and for certain kinds of like other higher value tokens you might want to say we want to wait for 25 or even 100 blocks to be on top of it
- 25:01
so why does this live in the client and not in the connector so this one's saying that we believe for example that after 30 blocks it it's impossible to construct a fork from the past so we say that you only need 30 blocks um to for example to be able to construct a fork from any of these blocks so how you prove that like a new chain becomes canonical so you can have a fork let's say five blocks in the past
- 25:43
that builds two chains in parallel so there's like active 51 attack going on like and then the canonical chain is the one with the proof of work but let's say the parallel chain that's going on is slightly less so when it produces a new block at a given height it will exceed the power of work like the the amount of work but at the same time your like previous canonical also produce a block at this site and now it exits the the proof of work
- 26:23
so yeah they can build in parallel until at some moment uh they run out of work like some chain of bins so we say in 30 blocks it cannot happen anymore by this client so like so so at some point we'll cut this chain out of out of the like canonical and like garbage collect blocks or headers that were like from the fork okay so that's just like core how you have to implement any ethereum client like client or no
- 27:10
you have to you have to code in some gc threshold basically some some measure of finality well you don't have to but it's probably optimization so you don't need to like keep them around after this i understand like i understand what it's for i guess i'm just i'm i'm not asking why we have it i'm asking why it lives here and not in the like why can't connectors specify their own finality concerns
- 27:43
well they can it's not this use case 10 is enough for this one we want 30. yeah if we don't tell which is a final we just tell after which we will not keep them so after 30 blocks it will not keep the headers potentially so this is kind of max yeah and it's like you need to like pre provide them again i'm not sure if you can do this so like once they finalize i don't think you can fork it
- 28:14
anymore because it will reject the header that is so far behind as like non-final and can be garbage collected um so you have canonical header hashes so the given height it will give the best header right now it gives it keeps all header hashes um which is a vector of all forks there at a given height i think them it can be abused if you like can meet mines so many blocks that are appropriate forks of a given block probably you cannot do this because uh you only store a hash here so
- 29:06
it shouldn't be an issue but we use this vector when we do garbage collection for finalization and we keep headers so that's actual header from ethereum that is given to us finally infos so is interesting basically it has a total difficulty apparent hash and a number number is like height and you you need to info if you want to compare two of them together you don't need to like entire
- 29:48
header to look at it just need the information for the given header and finally we have a trusted signer so this is like for proof of uh authority testnet apparently now what we do we stick a canonical header hash then we add our new header hash at a given height then we use we put the actual header into the list and we create an info same total difficulty zero parent hash is unknown and this
- 30:31
is the height like this so that's an initialization we created a blockchain that we trust at a given height this our new genesis from which will track entire chain all right so uh this is a view method that gives a merkle root at a given height that's for validation so if you want to validate it that the block is or the the installation is correct through a view call uh this one gives the last block number using the
- 31:11
infos and the best header hash the best header hashes like our canonical bust header this maximum proof of work now block hash for the given index it actually gives you a hash from the canonical hashes so the idea is your last block number and a block hash should give you some type of finality ah here's a number of confirmations so this is another one for safe as you mentioned so like for most of
- 32:01
them 25 is enough uh and finalization is 500 blocks so it's pretty far quite safe and we use you can use block hash save so you say what is the canonical save block that is 25 confirmations on top so it has a chain that is built 25 valid headers on top it also can give you all the last one um this is the same one as block save oh it is okay it's not it's a last on top so like it's a tail the best tail okay the chain
- 33:01
like leaf so block chains kind of dag so finally the the most interesting method is add a block header and how it works so it again uses borsch and also returns boards for some reason because it doesn't return anything actually if we first get the header we deserialize it so this is a proof that uh the header this is approved from the dag merkle tree root the header contains the epoch so we know the epoch
- 33:55
and then we will be able to verify and compute it hash efficiently using this proof so we first deserialize if it's a proof of work then we verify that this is proof of work if not then we need to verify that the header is valid now we find the previous header apparent we give current header apparent and the history to our method that runs verify header and then we record this header on chain so first let's look at the
- 34:33
verified header so this is a inner method so it's not exposed it has header the parent and proof we do hashimoto merkel so this actual work that happens um we find the merkle root for the given e-book based on the header number then we do we hash has the given header we actually have it uh nonce and then we go up the tree until we can reach the root and the root should
- 35:42
match or maybe you just return the route so we have the hash that we computed it has to be computed not the the given hash from a given header and we compare whether it's part of the difficulty that is set for this header and we verify gas limits we verify so this is logic from a theory in the current configuration so if ethereum configuration gonna hard fork we'll need to adjust this logic that's why we need upgradability on this
- 36:40
contract to to be able to change this logic in uh in simple question land um so what what is a difficulty actually look like i believe it's uh you i'm guessing it's a very large number with a bunch of zeros um and then all ones or or how does that how does that look exactly it's okay if we don't want to answer this too if you want to keep it yeah yeah so it's like just a bunch of zeros in front
- 37:14
so if you put it in a binary presentation then difficulty is number of zeros in front of the number so so when it's like less difficult more difficult it's literally like one less zero one more zero question here is like why are we using the header and not the previous header if i just set the difficulty on my header to whatever i want but the difficulty is going to be like a number like 10 or 11 and that's going to say how many leading zeros the hash
- 37:52
needs to have no really they understand exactly this this is completely broken logic like why we verify this it's always true what's going on here it's kind of bug yeah we should follow up on this it seems like some of this header should be actual proof and we don't use it i'm pretty sure this one should be pre foundation should it be fine maybe yeah this one should be actual header difficulty maybe elsa brave because previous
- 38:32
defines the difficulty that you need to do and this should be a next difficulty so that seems like a bug that you can right now abuse unless i'm missing something because i can just specify a difficulty of zero and mint a new header locally and it will be valid you can submit stuff directly to this contract over and over and like 51 attack ethereum or near the near client representation of ethereum the total difficulty will not be the
- 39:20
correct one um because the canonical will have a higher difficulty than what i my my headers that i'll produce but i can clearly break the contract logic by going very far in finality that's what i'm saying and it sounds like you can essentially 51 percent attack like this client's view i just can yeah i can go and just produce 500 headers and they will be considered canonical so no one will be able to build on top of me plus i just need 25 to be considered
- 40:06
and then you can start like sending your near address whatever i want on like yeah anyway cool um all right let's go back to now record great attack record record header record record header is takes a valid block that we just verified it's just proof of authority or proof of work and we update the canonical logic do garbage collection and all the stuff so we like we update best in for compute
- 40:56
so for example header is too old to be on canonical chain and stuff like this we get parent again then compute we insert this header compute new difficulty and there is just a tie breaker then we put it into canonical header hashes if if it's actually canonical and we do garbage collection and some old stuff if it's not a canonical we just record it because it means it's not a main canonical fork and
- 41:46
and we don't garbage collect yet so the only garbage collect all blocks when um threshold is old so this garbage collection it basically goes on a given height and just kills all the like non-canonical blocks headers uh from storage and uh for very old we kill all of them including the canonical one but only keep the hash of the header and i think that's it so we have 14 minutes so we can quickly look at the program if there's no questions on this contract
- 42:39
i i have a question maybe generally and it's okay if we don't want to go there um but i know that we've been talking about just kind of like generally the bridge like if the topic is kind of like the bridge we've been talking about having an upgradeable bridge uh for a little while now and it seems like there's a there's an issue there and and i i'm curious like if we know what is so tricky about that um or not so grateable breach
- 43:12
it basically need to handle a hard fork we don't want to contract that like mintable to to be able to to switch manually so you don't want to like because you will not be able to unlock them on ethereum site because or maybe you will be able but it will require fork in the fungible tokens and everything that depended on a canonical chain but you probably don't want to do this and that great ability needs to be able
- 43:53
to handle this hard fork to the new proposal once it exceeds like 51 or whatever the protocol charge called on ethereum then it should switch to the new chain that is hard forked version and we need to point all contracts to the new prover that will use the new client that is like a new version of the block so it it needs to seamlessly work after some points so for example approver
- 44:31
it's to just magically switch to the correct um correct chain because the card fork on ethereum is actually in your chain anyway let's um have this separately so prover is actually not as large contract but the way it works is it has a method called uh assert is client hash so this can be used standalone so you just give a block number and expected block hash and it calls safe
- 45:21
on on the client side so it gets the hash from the client side so from the blockchain canonical blockchain and it verifies uh that it matches using inner method on block hash which is just verifies that it's a callback to self and then it compares them and returns true openings and the second is verify log entry so this an upgrade so you give log index log entry data receipt index
- 46:00
received data header data proof skip bridge call well um okay okay so first what you do you have a header you verify locally from the header that the given log entry exists using the merkle proof from the root overseas once you've done this you need to call bridge client to verify the given block is correct you can do this uh with this it seems like this is just a little bit weird because the gas
- 47:03
hash here is different like gas used here is different so in here you can just call assert locally and return this promise finally and this is just because our gas is very confusing and we can replace it as soon it's like 100 targets or something the entire gas that enter i guess is enough but you don't need this to be all the rest of the prepaid because it should be also like five tier i guess
- 47:43
now verified try proof is actually just technical implementation that verifies the ethereum logic on your site so pro is also fairly simple nothing like complicated there also i feel like there's another security issue is we don't verify the hash of the block or maybe it's verified to decode go ahead so one thing i learned which was awesome is uh the whole borscht serialization thing and we need
- 48:40
a schema for that and we were looking at the init function but then i i think i probably missed it thinking about other things but uh where is the where was the schema for this for bush yes or four years yeah so for this uh it automatically constructed from this schema so you give types here so it's just a schema so basically if you remove if you remove this part and then wrap it
- 49:12
into a structure then it's gonna be the schema gotcha okay thanks so yeah so there is no problem with header hashing because hashing is done during the decoding so you cannot really fake the cache so yeah so this one is fine we only need to verify that the client logic is not like seems to be invalid any questions on these two contracts one of the the last question i think i have is um you said earlier that some things are garbage collected like headers or garbage collected and but if you need to go back then you
- 50:07
could say oh hey here's here's a header and it will be able to figure out the hash of that and complete it basically so does that mean that a minimal amount of the entire chain is saved or is like everything garbage collected past a certain point okay it has uh caches it has historic hashes only so it's always growing but only by the size of the cache so hash is like height cache probably info is not garbage collected so info is also there so it's like
- 50:47
reasonably large so that has constant demand from the storage gotcha thanks okay i'll stop presenting and maybe some final questions and before we close i think we can call this one live security contract review yeah consider the last two that's true well thanks everyone um hope to enjoy the session and learn more