Closed sirlensalot closed 5 years ago
Chainweb Servant API:
type ApiV1API =
SendApi :<|>
PollApi :<|>
ListenApi :<|>
LocalApi
type SendApi = "send"
:> ReqBody '[JSON] Pact.Types.API.SubmitBatch
:> Post '[JSON] Pact.Types.Api.RequestKeys
type PollApi = "poll"
:> ReqBody '[JSON] Pact.Types.API.Poll
:> Post '[JSON] Pact.Types.API.PollResponses
type ListenApi = "listen"
:> ReqBody '[JSON] Pact.Types.API.ListenerRequest
:> Post '[JSON] Pact.Types.API.ApiResult
type LocalApi = "local"
:> ReqBody '[JSON] (Pact.Types.Command.Command Text)
:> Post '[JSON] (Pact.Types.Command.CommandSuccess Value)
Pact server API:
type ApiV1API =
( "send" :> ReqBody '[JSON] SubmitBatch :>
Post '[JSON] RequestKeys
:<|> "poll" :> ReqBody '[JSON] Poll :>
Post '[JSON] PollResponses
:<|> "listen" :> ReqBody '[JSON] ListenerRequest :>
Post '[JSON] ApiResult
:<|> "local" :> ReqBody '[JSON] (Command Text) :>
Post '[JSON] (CommandSuccess Value)
)
type PactServerAPI =
"api" :> "v1" :> ApiV1API
:<|> "verify" :> ReqBody '[JSON] Analyze.Request :> Post '[JSON] Analyze.Response
:<|> "version" :> Get '[PlainText] Text
Kadena API:
api :: Api ()
api = route [
("send",sendPublicBatch)
,("poll",poll)
,("listen",registerListener)
,("local",sendLocal)
,("private",sendPrivateBatch)
,("config", sendClusterChange)
]
-- note all handlers catch and log exceptions;
-- functions like `readJSON` use `die`
sendLocal = ... do
-- 'Command Text' request
(cmd :: Pact.Command BS.ByteString) <- fmap encodeUtf8 <$> readJSON
...
-- read `Value` from MVar; this is a _crResult from CommandResult
writeResponse $ ApiSuccess $ r
sendPublicBatch = ... do
SubmitBatch cmds <- readJSON
...
queueRpcs rpcs
... -> writeResponse $ ApiSuccess rks
poll = ... do
(Poll rks) <- readJSON
...
writeResponse $ pollResultToReponse possiblyIncompleteResults
... -> ApiSuccess $ PollResponses $ scrToAr <$> m
registerListener = do
...
(ListenerRequest rk) <- readJSON
...
return $ ApiSuccess $ scrToAr scr
...
-- also is the only endpoint to actually throw a caught exception
throw e
sendPrivateBatch = ... do
...
SubmitBatch cmds <- readJSON
...
queueRpcs rpcs
sendClusterChange = ... do
SubmitCC ccCmds <- readJSON
...
queueRpcs [finalRpc]
queueRpcs :: [(RequestKey,CMDWire)] -> Api ()
queueRpcs rpcs = do
-- rks :: 'RequestKeys'
writeResponse $ ApiSuccess rks
-- including to show how ApiResult is populated
scrToAr :: CommandResult -> ApiResult
scrToAr cr = case cr of
SmartContractResult{..} ->
ApiResult (toJSON (Pact._crResult _scrResult)) (Pact._crTxId _scrResult) metaData'
ConsensusChangeResult{..} ->
ApiResult (toJSON _concrResult) tidFromLid metaData'
PrivateCommandResult{..} ->
ApiResult (handlePR _pcrResult) tidFromLid metaData'
where metaData' = Just $ toJSON $ _crLatMetrics $ cr
tidFromLid = Just $ Pact.TxId $ fromIntegral $ _crLogIndex $ cr
handlePR (PrivateFailure e) = toJSON $ "ERROR: " ++ show e
handlePR PrivatePrivate = String "Private message"
handlePR (PrivateSuccess pr) = toJSON (Pact._crResult pr)
die :: String -> Api t
die res = do
_ <- getResponse -- chuck what we've done so far
setJSON
log res
writeLBS $ encode $ (ApiFailure ("Kadena.HTTP.ApiServer" ++ res) :: ApiResponse ())
finishWith =<< getResponse
ApiSuccess
is ApiResponse
in Pact 2.4.1: https://github.com/kadena-io/pact/blob/8f4216d274749507158c5e23be8c4b32a75191a6/src-ghc/Pact/Types/API.hs#L43
data ApiResponse a =
ApiSuccess
{ _apiResponse :: !a} |
ApiFailure
{ _apiError :: !String}
Note that Pact as of 2.6.1 still used ApiResponse
, so the lack of this in other APIs must have been introduced in changes leading up to testnet.
ApiResponse
was removed in https://github.com/kadena-io/pact/pull/368.
send
Request: SubmitBatch
.
newtype SubmitBatch = SubmitBatch { _sbCmds :: [Command Text] }
Response:
RequestKeys
.ApiResponse RequestKeys
.poll
Request:Poll
newtype Poll = Poll { _pRequestKeys :: [RequestKey] }
Response:
PollResponse
.ApiResponse PollResponse
PollResponse
:
newtype PollResponses = PollResponses (HM.HashMap RequestKey ApiResult)
ApiResult
:
data ApiResult = ApiResult {
_arResult :: !Value,
_arTxId :: !(Maybe TxId),
_arMetaData :: !(Maybe Value)
} deriving (Eq,Show,Generic)
ApiResult encoding: kadena:
pact -s:
Chainweb:
CommandResult
, CommandSuccess
, CommandError
, Receiptsdata CommandResult = CommandResult
{ _crReqKey :: RequestKey
, _crTxId :: Maybe TxId
, _crResult :: Value
, _crGas :: Gas -- not in Kadena
} deriving (Eq,Show)
Kadena:
uses pact 2.4.1 jsonResult
:
jsonResult :: ToJSON a => ExecutionMode -> RequestKey -> a -> CommandResult
jsonResult ex cmd a = CommandResult cmd (exToTx ex) (toJSON a)
a
(i.e., _crResult
) is CommandSuccess
or CommandFailure
.
pact -s: same
chainweb: same. Also, _crResult
is included of HashedTxLogOutput
, ie the receipt, as the "output" to the SPV system/payload header. API might consider supplying the Full output. Receipts definitely need more that just _crResult
.
data FullLogTxOutput = FullLogTxOutput
{ _flCommandResult :: A.Value -- note this is just _crResult not a full CommandResult
, _flTxLogs :: [TxLog A.Value]
} deriving (Show, Eq)
data HashedLogTxOutput = HashedLogTxOutput
{ _hlCommandResult :: Value -- see note above about _crResult
, _hlTxLogHash :: Hash
} deriving (Eq, Show)
CommandSuccess
, CommandError
:
data CommandError = CommandError {
_ceMsg :: String
, _ceDetail :: Maybe String
}
instance ToJSON CommandError where
toJSON (CommandError m d) =
object $ [ "status" .= ("failure" :: String)
, "error" .= m ] ++
maybe [] ((:[]) . ("detail" .=)) d
data CommandSuccess a = CommandSuccess {
_csData :: a
}
instance (ToJSON a) => ToJSON (CommandSuccess a) where
toJSON (CommandSuccess a) =
object [ "status" .= ("success" :: String)
, "data" .= a ]
CommandSuccess:
kadena: populated with last erOutput
from EvalResult
([Term name]
)
pact -s: same
chainweb: same
Make CommandResult
uniform/ubiquitous over the API
PollResponses
will replace ApiResult
with CommandResult
local
will return CommandResult
listen
will return CommandResult
Naming: consider PactResult
or ExecResult
, also consider ...Response
Changes:
[TxLog]
or hash, perhaps parameterize thisMaybe GasOutput
~
Either e TermOutput
e
being a string or exception objectTermOutput
is maybe ReadableTerm
or somethingTerm Name
that can roundtrip through JSON.TermOutput
s can become Term Name
but not vice-versa~ PactValue nowMaybe ContinuationResult
ContinuationResult
being a mutated PactExec, having pactId, step, executed, Yield
Yield
having the yield value (as a TermOutput object, or just the object part), endorsement, target chain(Name, [TermOutput])
as discussed below.Metadata
as a platform-specific performance metadata object
GasOutput
here and any other data it sees fitEDIT: something like
CommandResult l = CommandResult {
{ _crReqKey :: RequestKey
, _crTxId :: Maybe TxId
, _crResult :: Either PactError PactValue
, _crGas :: Gas
, _crLogs :: l -- this would be [TxLog Value] in pact -s
, _crContinuation :: Maybe PactExec
, _crMeta :: Maybe Value }
local
Request:
Command Text
Response:
kadena: ApiSuccess
with _crResult
populated as above (CommandSuccess/CommandError)
pact -s: CommandSuccess
as above
chainweb: CommandSuccess
as above
listen
Request:
ListenerRequest
newtype ListenerRequest = ListenerRequest RequestKey
Response:
kadena: ApiResult
same as poll
pact -s: ApiResult
same as poll
chainweb: ApiResult
same as poll
Draft of goals in unification:
ApiResponse
, with caveat that it might be complicated to get Servant to play nice. See https://stackoverflow.com/questions/41753516/custom-json-errors-for-servant-serverconfig
endpoint explicitly as it is already hammered into Pact-specific concepts like txid. private
is essentially identical to send
as it is.Value
and polymorphism where appropriate. CommandResult
being the main violator).[TxLog]
hash, etc.CommandResult/Success/Error
must be preserved to semantically indicate "valid failures". However this should be rationalized into a single type.Pact continuations are handled with send
(or private
) within the payload ContMsg
. Responses vary significantly based on whether a private pact is executed (in kadena), a public pact (in pact -s
and chainweb) and, in the future, SPV (chainweb
but all platforms ideally).
Request:
Command Text
with ContMsg
payload.
kadena/private/pact 2.4.1:
data ContMsg = ContMsg
{ _cmTxId :: !TxId -- "pact id"
, _cmStep :: !Int -- step idx
, _cmRollback :: !Bool -- rollback flag
, _cmResume :: !(Maybe Value) -- `yield` value made from pact `object`
} deriving (Eq,Show,Generic)
pact -s, chainweb:
data ContMsg = ContMsg
{ _cmPactId :: !PactId -- now a proper datatype
, _cmStep :: !Int -- step idx
, _cmRollback :: !Bool -- rollback flag
, _cmData :: !Value -- payload message data (note that kadena lacks this)
} deriving (Eq,Show,Generic)
Response:
kadena/private:
CommandSuccess
output of pact erResult
, plus automatic dispatch of subsequent continuation request with yield
value.
pact -s, chainweb:
CommandSuccess
output of pact erResult
.
Internal state:
kadena/private:
Tracking of pacts by pact ID with Pact
datatype. Note that yield/resume is not tracked.
data Pact = Pact
{ _pTxId :: TxId -- pact ID
, _pContinuation :: Term Name
, _pSigs :: S.Set PublicKey -- note trust model implied here as resumes are automatic
, _pStepCount :: Int
}
pact -s/chainweb:
Tracking of pacts by pact ID directly using PactExec
result from pact interpreter. Yield/resume is tracked; continuation is strict App
of args.
-- | Result of evaluation of a 'defpact'.
data PactExec = PactExec
{ -- | Count of steps in pact (discovered when code is executed)
_peStepCount :: Int
-- | Yield value if invoked
, _peYield :: !(Maybe (Term Name))
-- | Whether step was executed (in private cases, it can be skipped)
, _peExecuted :: Bool
-- | Step that was executed or skipped
, _peStep :: Int
-- | Pact id. On a new pact invocation, is copied from tx id.
, _pePactId :: PactId
-- | Strict (in arguments) application of pact, for future step invocations.
, _peContinuation :: PactContinuation
} deriving (Eq,Show)
newtype PactContinuation = PactContinuation (App (Term Ref))
For SPV, yield results and endorsements (see #443 and related) will need to become part of the receipt; most transparent way to do this is encode all of this in the API result; in general all of the data loss for API results is not desirable.
Maybe Value
for the yield and no need for data
as the user is never sending the message. data
field for user-submitted data after initial invocation of the pact.Conclusion:
Private continuations should be sent via a different endpoint and JSON type, which in turn will encode the Yield
value into data
. Since this is intended to be a machine-generated request this will avoid confusion about ContMsg
as a JSON type for user use.
EDIT on second thought this is unsatisfying.
Three kinds of pacts, and implications for resume, user data, and argument rehydration:
_peContinuation
.TermOutput
Continuation
type should probably support this better; currently it is a directly-evaluable (App (Term Ref))
but could just as well be (Name,[TermOutput])
or at least (App TermOutput)
(from which Name can be derived).Name
and [TermOutput]
need to be in the proof._cmData
due to automated execution.Yield
will be created/specified per #443 and in CommandResult above.
Continuation as (Name,[TermOutput])
is specified in CommandResult above.
Proposed ContMsg
:
data ContMsg = ContMsg
{ _cmPactId :: !PactId -- now a proper datatype
, _cmStep :: !Int -- step idx
, _cmRollback :: !Bool -- rollback flag
, _cmData :: !(Object Name) -- or whatever the TermOutput safe version of this is
, _cmProof :: !(Maybe ByteString) -- or some specific proof object?
} deriving (Eq,Show,Generic)
Private messages will simply overload _cmData
with Yield data. One day endorsements could make sense as Petersen commitments or something, and target would indicate entity. Maximum future-proofing would suggest Private uses Yield and _cmData
be encoded as an ADT like:
data ContData = ContUserData !(Object Name) | ContPrivYield !Yield
But again we would probably like to see this come in another endpoint, implying that the API-visible type should not bother with this.
Command Text
/Command (Payload ParsedCode m)
Note that the full specification/transformation is:
Command Text
via API JSONCommand ByteString
in API handlersCommand (Payload Text m)
: intermediate step in verifyCommand
, where the Text
is the code in ExecMsg.Command (Payload ParsedCode m)
: Code is parsed, final output of verifyCommand
.data Command a = Command
{ _cmdPayload :: !a
, _cmdSigs :: ![UserSig]
, _cmdHash :: !Hash
}
data Payload m c = Payload
{ _pPayload :: !(PactRPC c)
, _pNonce :: !Text
, _pMeta :: !m
}
data PactRPC c =
Exec (ExecMsg c) |
Continuation ContMsg
data ExecMsg c = ExecMsg
{ _pmCode :: c
, _pmData :: Value
}
data ContMsg = ContMsg
{ _cmPactId :: !PactId
, _cmStep :: !Int
, _cmRollback :: !Bool
, _cmData :: !Value
}
data ParsedCode = ParsedCode
{ _pcCode :: !Text
, _pcExps :: ![Exp Parsed]
}
Metadata types:
newtype PrivateMeta = PrivateMeta { _pmAddress :: Maybe Address }
data PublicMeta = PublicMeta
{ _pmChainId :: Text
, _pmSender :: Text
, _pmGasLimit :: ParsedInteger
, _pmGasPrice :: ParsedDecimal
}
Argument for error codes: https://medium.com/@sunitparekh/guidelines-on-json-responses-for-restful-services-1ba7c0c015d
Here's an argument for both:
for example the foursquare API provides HTTP status return codes and then also sends those codes in the response body. http://www.startupcto.com/backend-tech/building-an-api-best-practices#handling-errors
My $.02 is that "you have to handle 500 etc anyway", so error codes are a thing, and it does clean up the API. The practice above of including a nice JSON is good anyway, so we can try to use modifyResponse
to do this.
@slpopejoy
change _crResult to Either e TermOutput, e being a string or exception object
Would e
then be a type parameter of CommandResult
or should it just be String?
And TermOutput
becomes PactOutput
?
Would e then be a type parameter of CommandResult or should it just be String? And TermOutput becomes PactOutput?
@LindaOrtega it should probably be PactError
, and PactValue
as the output type.
➤ anagha@kadena.io commented:
Question - do we need Stuart to review PR as well?
➤ anagha@kadena.io commented:
Not all of this is required for May 30th
@slpopejoy for the ContMsg proposal:
data ContMsg = ContMsg
{ _cmPactId :: !PactId -- now a proper datatype
, _cmStep :: !Int -- step idx
, _cmRollback :: !Bool -- rollback flag
, _cmData :: !(Object Name) -- or whatever the TermOutput safe version of this is
, _cmProof :: !(Maybe ByteString) -- or some specific proof object?
} deriving (Eq,Show,Generic)
Should _cmData
be of type !(ObjectMap PactValue)
, which is the same data type as the Yield field in PactExec? Moreover, should the data section of ExecMsg also be of this type?
Moreover, should I create a proof object for _cmProof
or is ByteString/Text fine here?
@LindaOrtega
_cmData
is not yield but the usual user data so it should match ExecMsg
, ie Value
, sorry for the confusion there~I wonder if we should enforce in Pact 3.0 that user data is always an object though, and break ExecMsg
too. In which case it would be ObjectMap PactValue
for both~ EDIT nevermind, this would break how read-integer
and other functions work
_cmData
is user data and Object Name
is currently a little ambiguous. However, discrimination will be easier when #480 is in, since continuation yield/resume data will be a Yield
object with a potential endorsement.
@emilypi the question for you is the _cmProof
format, I answered re _cmData above https://github.com/kadena-io/pact/issues/451#issuecomment-494471018
_cmProof
would be easiest to work with as a Maybe Hash
, since that's the end result of an Endorsement
if it's present. If we want to represent that as a bytestring, that's fine, but we should make a note that it's just the bytestring associated with a Hash. Hash would be better.
@emilypi @slpopejoy based on both of your inputs, ContMsg should look as follows correct?
data ContMsg = ContMsg
{ _cmPactId :: !PactId
, _cmStep :: !Int
, _cmRollback :: !Bool
, _cmData :: !Value
, _cmProof :: !(Maybe Hash)
} deriving (Eq,Show,Generic)
Edit: 05/21/2019 Changes per offline conversation
data ContMsg = ContMsg
{ _cmPactId :: !PactId
, _cmStep :: !Int
, _cmRollback :: !Bool
, _cmData :: !Value
, _cmProof :: !(Maybe ByteString)
} deriving (Eq,Show,Generic)
Use modifyResponse to have "lucid errors", ie HTTP error codes but also a full JSON { "status": "failure", "code": 402, "Bad Input: you got your chocolate in my peanut butter" }
@slpopejoy I have some questions related to this proposal:
The Servant API already outputs http error codes (i.e. 404 if you try to ping a non-existent endpoint) via ServantError. Is it that we want to convert PactErrors into 400 (or some other http code) using something like throwErr
inside the Servant Handler code (see this Chainweb code for an example).
Is the intent of this proposal to make ServantError
(See definition)'s errBody
field be of type Value
(i.e. JSON)?
Do we still want to reintroduce ApiResponse
per this comment?
➤ Linda@kadena.io commented:
3/4 tasks have been merged in.
➤ anagha@kadena.io commented:
Moved the 4th task to its own separate issue (#P515).
Will wait for Linda to get back and close this issue
Closed via https://github.com/kadena-io/pact/pull/505 and https://github.com/kadena-io/pact/pull/504. 4th task moved to its own issue: https://github.com/kadena-io/pact/issues/515
Research issue for finalizing Pact API across Chainweb,
pact -s
, kadena (scalableBFT).Proposals
CommandResult
overhaul (see "CommandResult proposal" https://github.com/kadena-io/pact/issues/451#issuecomment-479665427)ContMsg
improvements (see "ContMsg proposal" https://github.com/kadena-io/pact/issues/451#issuecomment-479758699)modifyResponse
to have "lucid errors", ie HTTP error codes but also a full JSON{ "status": "failure", "code": 402, "Bad Input: you got your chocolate in my peanut butter" }
Related/required are
Yield
type, cross-chainyield
overloadTermOutput
typeChainId
type and non-malleabilityPactId
Follow on Chainweb issues include using the exported Servant API (currently copy-pasted), supporting
Yield
and SPV, and otherwise supporting this upgrade.Servant API