-
Notifications
You must be signed in to change notification settings - Fork 132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add graphql branch and breaking changes #2987
Conversation
b80b7f7
to
664a300
Compare
21ffb77
to
4c44623
Compare
this way, it's possible to get all tips/main tip, and make a query in any of those. other changes: - Transaction { block } is now Transaction { blocks } - blockByChainLength is now blocksByChainLength - Block has a Branches field
it's not really necessary, as getting the confirmed branch and getting it from there creates less api redundancy also, update the schema
also, the usage of `first` in the allBlocks query didn't really make sense, as using `first` and `last` at the same time doesn't really make sense
4c44623
to
456ccb3
Compare
Block { | ||
hash: block.id(), | ||
contents: tokio::sync::Mutex::new(Some(block)), | ||
} | ||
} | ||
|
||
async fn get_explorer_block(&self, db: &ExplorerDB) -> FieldResult<Arc<ExplorerBlock>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could rename to fetch_explorer_block
to reflect that it caches the retrieved block now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more a lazy init than a cache
, the only reason to do this is to not fetch the block unless some of the required fields need it, and it's saved so that if two fields need it then it's not fetched twice.
I agree with the name change anyway.
To be honest this doesn't really belong in this PR, especially because for block you almost always want the block anyway, except in the rare case that you ask for a paginated list of hashes but don't ask for any fields.
} | ||
|
||
async fn get_contents(&self, context: &Context) -> FieldResult<ExplorerTransaction> { | ||
if let Some(c) = &self.contents { | ||
Ok(c.clone()) | ||
} else { | ||
let block = self.get_block(context).await?; | ||
//TODO: maybe store transactions outside blocks? as Arc, as doing it this way is pretty wasty | ||
let block = &self.get_blocks(context).await?[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can there be a get_block
operation if you already know you can use a block with the specific hash?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if I understand. There will always be at least a block there, you mean a function that gets that one?
To be honest, this query is not designed really well, getting the block to get the transaction body is kinda bad, because it also means transactions are duplicated if they are in multiple blocks, which is kinda a waste of memory.
Fetching the entire block to get only one transaction it's probably no ideal either.
I didn't want to beat around the bush too much in this PR, so I just left what has equivalent behavior to the previous thing. (Also, extracting transaction bodies from the blocks is not that trivial anyway, because of gc and other things).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean it's already wasteful to retrieve several blocks if you only need one, and you can just pick the first of self.block_hashes
and query just that block by its hash.
Optimizing to be able to query individual fragments straight from the store/db would be good (and might be possible with some zero-copy block storage access and iteration on raw fragments in a raw block), but this is a topic for a more substantial rework that would involve the storage layer, so not for this PR. Cc @eugene-babichenko
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, that... Yeah, sure, makes sense.
Well, considering that the explorer doesn't even use the storage, yeah, that's a big change. I'm not sure we want to do that, though? And using raw fragments wouldn't play that nice with the way we show utxo inputs now, I guess.
@@ -1328,6 +1520,30 @@ impl Query { | |||
Transaction::from_id(id, context).await | |||
} | |||
|
|||
/// get all current tips, sorted (descending) by their length | |||
async fn tips(&self, context: &Context) -> Vec<Branch> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use tip
in the node protocol for the currently selected branch.
Maybe use branch_heads
or just branches
, considering the return type is Vec<Branch>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, now that you bring that up... I guess what I'm calling Branch
in the explorer/api is more like what's called Ref
in the ledger.
I'm not entirely convinced that name makes sense for the public api, though.
And also I don't think that queries should be limited to heads.
But maybe it makes sense to call the thing Ref
, StateRef
, or something like that (not commit unfortunately).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ref
is OK in internal code, but for the public explorer API we should stick to the nomenclature used elsewhere. I think "braches" are close in meaning to the concept used in git (and git has a HEAD
alias for what we mean by the "tip").
|
||
/// get the block that the ledger currently considers as the main branch's | ||
/// tip | ||
async fn main_tip(&self, context: &Context) -> Branch { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is just tip
in the node protocol, let's be consistent.
transaction(id: String!): Transaction! | ||
|
||
"""get all current tips, sorted (descending) by their length""" | ||
tips: [Branch!]! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this should be branches
to be consistent with the node protocol's terminology...
get the block that the ledger currently considers as the main branch's | ||
tip | ||
""" | ||
mainTip: Branch! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this is tip
.
type VotePayloadPrivateStatus { | ||
proof: String! | ||
encryptedVote: String! | ||
} | ||
|
||
type VotePayloadPublicStatus { | ||
choice: Int! | ||
} | ||
|
||
union VotePayloadStatus = VotePayloadPublicStatus | ||
union VotePayloadStatus = VotePayloadPublicStatus | VotePayloadPrivateStatus | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good, we missed that.
More descriptive message for an explorer error when the block containing a fragment/transaction cannot be fetched.
see #2963
This branches from #2981 instead of master because that contains the core logic needed for this to work, but I think the separation is helpful because:
a) This is the PR that involves the graphql breaking changes, so merging the previous one by itself is a bit safer.
b) This can be reviewed independently in general, as just looking at the
schema.graphql
diff gives an idea of what this does. Thejormungandr/explorer/graphql/mod.rs
serves the same purpose, although it's a bit more verbose as I had to move entire functions to theBranch
type, and that causes a big diff even if the implementation is basically the same.Idea
With these changes it should be possible to:
Know which block is the current tip.
Know which blocks are considered tips (which means, which blocks are the branches).
Make queries contextualized to a branch, either just whatever the current one is, or by providing a hash.
Breaking changes
allBlocks
(now justblocks,
probably should change the other ones too),allStakePools
,allVotePlans
are now inBranch
instead ofQuery
(the graphql root)Address { transactions }
is now inBranch
too, undertransactionsByAddress
.Epoch { blocks }
, underblocksByEpoch
Transaction { block }
is nowTransaction { blocks }
(in plural), so it returns a list instead.blockByChainLength
is nowblocksByChainLength
(in plural), so it returns [Block!]! instead of Block.status
is nowsettings
andStatus
is nowSettings
, as it was a weird type anyway. It also doesn't contain the latest block, as that can be queried throughQuery { mainTip { block { .. } } }
now.Non-breaking changes
mainTip
andtips
. These allow to retain the current behavior of most queries by usingmainTip
to scope the results.branch
can be used to get a branch by it's hash. This will return a not found error if the branch is not in memory anymore.Address { confirmed_transactions }
. Although, this is a bit extra.Conceptual
This introduces a
Branch
graphql type. It's identified by the hash of aBlock
. The difference between the two is that theBlock
is the block by itself, let's say, a collection of transactions with a parent pointer. TheBranch
represents the multiverse state produced by the application of that block.The distinction is mostly because the idea is to garbage collect the multiverse, but that also includes states that are in the mainchain, so you may have a
Block
(on disk, when we implement that) that has noBranch
(in memory), which means we can't answer queries that depend on the state at that point (tx's by address, delegation, let's say). Although, it's probably possible to design a db (using chain length to timestamp/filter) schema that can account for this, I'm not sure if it is an actual use-case.