/
executor.go
263 lines (226 loc) · 8.99 KB
/
executor.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
// Package commitment defines a roothash commitment.
package commitment
import (
"fmt"
"github.com/oasisprotocol/oasis-core/go/common"
"github.com/oasisprotocol/oasis-core/go/common/cbor"
"github.com/oasisprotocol/oasis-core/go/common/crypto/hash"
"github.com/oasisprotocol/oasis-core/go/common/crypto/signature"
"github.com/oasisprotocol/oasis-core/go/roothash/api/block"
"github.com/oasisprotocol/oasis-core/go/roothash/api/message"
)
var (
// ExecutorSignatureContext is the signature context used to sign executor
// worker commitments.
ExecutorSignatureContext = signature.NewContext(
"oasis-core/roothash: executor commitment",
signature.WithChainSeparation(),
signature.WithDynamicSuffix(" for runtime ", common.NamespaceHexSize),
)
// ComputeResultsHeaderSignatureContext is the signature context used to
// sign compute results headers with RAK.
ComputeResultsHeaderSignatureContext = signature.NewContext("oasis-core/roothash: compute results header")
)
// ComputeResultsHeader is the header of a computed batch output by a runtime. This
// header is a compressed representation (e.g., hashes instead of full content) of
// the actual results.
//
// These headers are signed by RAK inside the runtime and included in executor
// commitments.
//
// Keep the roothash RAK validation in sync with changes to this structure.
type ComputeResultsHeader struct {
Round uint64 `json:"round"`
PreviousHash hash.Hash `json:"previous_hash"`
// Optional fields (may be absent for failure indication).
IORoot *hash.Hash `json:"io_root,omitempty"`
StateRoot *hash.Hash `json:"state_root,omitempty"`
MessagesHash *hash.Hash `json:"messages_hash,omitempty"`
// InMessagesHash is the hash of processed incoming messages.
InMessagesHash *hash.Hash `json:"in_msgs_hash,omitempty"`
// InMessagesCount is the number of processed incoming messages.
InMessagesCount uint32 `json:"in_msgs_count,omitempty"`
}
// IsParentOf returns true iff the header is the parent of a child header.
func (h *ComputeResultsHeader) IsParentOf(child *block.Header) bool {
if h.Round != child.Round+1 {
return false
}
childHash := child.EncodedHash()
return h.PreviousHash.Equal(&childHash)
}
// EncodedHash returns the encoded cryptographic hash of the header.
func (h *ComputeResultsHeader) EncodedHash() hash.Hash {
return hash.NewFrom(h)
}
// ExecutorCommitmentFailure is the executor commitment failure reason.
type ExecutorCommitmentFailure uint8
const (
// FailureNone indicates that no failure has occurred.
FailureNone ExecutorCommitmentFailure = 0
// FailureUnknown indicates a generic failure.
FailureUnknown ExecutorCommitmentFailure = 1
// FailureStateUnavailable indicates that batch processing failed due to the state being
// unavailable.
FailureStateUnavailable ExecutorCommitmentFailure = 2
)
// ExecutorCommitmentHeader is the header of an executor commitment.
type ExecutorCommitmentHeader struct {
ComputeResultsHeader
Failure ExecutorCommitmentFailure `json:"failure,omitempty"`
// Optional fields (may be absent for failure indication).
RAKSignature *signature.RawSignature `json:"rak_sig,omitempty"`
}
// SetFailure sets failure reason and clears any fields that should be clear
// in a failure indicating commitment.
func (eh *ExecutorCommitmentHeader) SetFailure(failure ExecutorCommitmentFailure) {
eh.ComputeResultsHeader.IORoot = nil
eh.ComputeResultsHeader.StateRoot = nil
eh.ComputeResultsHeader.MessagesHash = nil
eh.ComputeResultsHeader.InMessagesHash = nil
eh.ComputeResultsHeader.InMessagesCount = 0
eh.RAKSignature = nil
eh.Failure = failure
}
// Sign signs the executor commitment header.
func (eh *ExecutorCommitmentHeader) Sign(signer signature.Signer, runtimeID common.Namespace) (*signature.RawSignature, error) {
sigCtx, err := ExecutorSignatureContext.WithSuffix(runtimeID.String())
if err != nil {
return nil, fmt.Errorf("signature context error: %w", err)
}
signature, err := signature.Sign(signer, sigCtx, cbor.Marshal(eh))
if err != nil {
return nil, err
}
return &signature.Signature, nil
}
// VerifyRAK verifies the RAK signature.
func (eh *ExecutorCommitmentHeader) VerifyRAK(rak signature.PublicKey) error {
if eh.RAKSignature == nil {
return fmt.Errorf("missing RAK signature")
}
if !rak.Verify(ComputeResultsHeaderSignatureContext, cbor.Marshal(eh.ComputeResultsHeader), eh.RAKSignature[:]) {
return fmt.Errorf("RAK signature verification failed")
}
return nil
}
// MostlyEqual compares against another executor commitment header for equality.
//
// The RAKSignature field is not compared.
func (eh *ExecutorCommitmentHeader) MostlyEqual(other *ExecutorCommitmentHeader) bool {
if eh.Failure != other.Failure {
return false
}
h1 := eh.ComputeResultsHeader.EncodedHash()
h2 := other.ComputeResultsHeader.EncodedHash()
return h1.Equal(&h2)
}
// ExecutorCommitment is a commitment to results of processing a proposed runtime block.
type ExecutorCommitment struct {
// NodeID is the public key of the node that generated this commitment.
NodeID signature.PublicKey `json:"node_id"`
// Header is the commitment header.
Header ExecutorCommitmentHeader `json:"header"`
// Signature is the commitment header signature.
Signature signature.RawSignature `json:"sig"`
// Messages are the messages emitted by the runtime.
//
// This field is only present in case this commitment belongs to the proposer. In case of
// the commitment being submitted as equivocation evidence, this field should be omitted.
Messages []message.Message `json:"messages,omitempty"`
}
// Sign signs the executor commitment header and sets the signature on the commitment.
func (c *ExecutorCommitment) Sign(signer signature.Signer, runtimeID common.Namespace) error {
if !c.NodeID.Equal(signer.Public()) {
return fmt.Errorf("node ID does not match signer (ID: %s signer: %s)", c.NodeID, signer.Public())
}
sig, err := c.Header.Sign(signer, runtimeID)
if err != nil {
return err
}
c.Signature = *sig
return nil
}
// Verify verifies that the header signature is valid.
func (c *ExecutorCommitment) Verify(runtimeID common.Namespace) error {
sigCtx, err := ExecutorSignatureContext.WithSuffix(runtimeID.String())
if err != nil {
return fmt.Errorf("roothash/commitment: signature context error: %w", err)
}
if !c.NodeID.Verify(sigCtx, cbor.Marshal(c.Header), c.Signature[:]) {
return fmt.Errorf("roothash/commitment: signature verification failed")
}
return nil
}
// ValidateBasic performs basic executor commitment validity checks.
func (c *ExecutorCommitment) ValidateBasic() error {
header := &c.Header.ComputeResultsHeader
switch c.Header.Failure {
case FailureNone:
// Ensure header fields are present.
if header.IORoot == nil {
return fmt.Errorf("missing IORoot")
}
if header.StateRoot == nil {
return fmt.Errorf("missing StateRoot")
}
if header.MessagesHash == nil {
return fmt.Errorf("missing messages hash")
}
if header.InMessagesHash == nil {
return fmt.Errorf("missing incoming messages hash")
}
// Validate any included runtime messages.
for i, msg := range c.Messages {
if err := msg.ValidateBasic(); err != nil {
return fmt.Errorf("bad runtime message %d: %w", i, err)
}
}
case FailureUnknown, FailureStateUnavailable:
// Ensure header fields are empty.
if header.IORoot != nil {
return fmt.Errorf("failure indicating body includes IORoot")
}
if header.StateRoot != nil {
return fmt.Errorf("failure indicating commitment includes StateRoot")
}
if header.MessagesHash != nil {
return fmt.Errorf("failure indicating commitment includes MessagesHash")
}
if header.InMessagesHash != nil || header.InMessagesCount != 0 {
return fmt.Errorf("failure indicating commitment includes InMessagesHash/Count")
}
// In case of failure indicating commitment make sure RAK signature is empty.
if c.Header.RAKSignature != nil {
return fmt.Errorf("failure indicating body includes RAK signature")
}
// In case of failure indicating commitment make sure messages are empty.
if len(c.Messages) > 0 {
return fmt.Errorf("failure indicating body includes messages")
}
default:
return fmt.Errorf("invalid failure: %d", c.Header.Failure)
}
return nil
}
// MostlyEqual returns true if the commitment is mostly equal to another
// specified commitment as per discrepancy detection criteria.
func (c *ExecutorCommitment) MostlyEqual(other OpenCommitment) bool {
h := c.Header.ComputeResultsHeader.EncodedHash()
otherHash := other.(*ExecutorCommitment).Header.ComputeResultsHeader.EncodedHash()
return h.Equal(&otherHash)
}
// IsIndicatingFailure returns true if this commitment indicates a failure.
func (c *ExecutorCommitment) IsIndicatingFailure() bool {
return c.Header.Failure != FailureNone
}
// ToVote returns a hash that represents a vote for this commitment as
// per discrepancy resolution criteria.
func (c *ExecutorCommitment) ToVote() hash.Hash {
return c.Header.ComputeResultsHeader.EncodedHash()
}
// ToDDResult returns a commitment-specific result after discrepancy
// detection.
func (c *ExecutorCommitment) ToDDResult() interface{} {
return c
}