diff --git a/config/consensus.go b/config/consensus.go index b6954231b7..a981baff15 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -422,6 +422,10 @@ var MaxEvalDeltaAccounts int // in a StateDelta, used for decoding purposes. var MaxStateDeltaKeys int +// MaxLogCalls is the highest allowable log messages that may appear in +// any version, used for decoding purposes. Never decrease this value. +const MaxLogCalls = 32 + // MaxLogicSigMaxSize is the largest logical signature appear in any of the supported // protocols, used for decoding purposes. var MaxLogicSigMaxSize int diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index b71ed02312..fa39bf77a3 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -1744,6 +1744,12 @@ "items": { "$ref": "#/definitions/AccountStateDelta" } + }, + "logs": { + "type": "array", + "items": { + "$ref": "#/definitions/LogItem" + } } } }, @@ -1910,6 +1916,24 @@ "format": "int64" } } + }, + "LogItem": { + "description": "Application Log", + "type": "object", + "required": [ + "id", + "value" + ], + "properties": { + "id": { + "description": "unique application identifier", + "type": "integer" + }, + "value": { + "description": " base64 encoded log message", + "type": "string" + } + } } }, "parameters": { @@ -2307,6 +2331,13 @@ "description": "\\[gd\\] Global state key/value changes for the application being executed by this transaction.", "$ref": "#/definitions/StateDelta" }, + "logs": { + "description": "\\[lg\\] Logs for the application being executed by this transaction.", + "type": "array", + "items": { + "$ref": "#/definitions/LogItem" + } + }, "txn": { "description": "The raw signed transaction.", "type": "object", diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index 08c6289b18..46e157c138 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -493,6 +493,13 @@ }, "type": "array" }, + "logs": { + "description": "\\[lg\\] Logs for the application being executed by this transaction.", + "items": { + "$ref": "#/components/schemas/LogItem" + }, + "type": "array" + }, "pool-error": { "description": "Indicates that the transaction was kicked out of this node's transaction pool (and specifies why that happened). An empty string indicates the transaction wasn't kicked out of this node's txpool due to an error.\n", "type": "string" @@ -1267,6 +1274,12 @@ "$ref": "#/components/schemas/DryrunState" }, "type": "array" + }, + "logs": { + "items": { + "$ref": "#/components/schemas/LogItem" + }, + "type": "array" } }, "required": [ @@ -1327,6 +1340,24 @@ ], "type": "object" }, + "LogItem": { + "description": "Application Log", + "properties": { + "id": { + "description": "unique application identifier", + "type": "integer" + }, + "value": { + "description": " base64 encoded log message", + "type": "string" + } + }, + "required": [ + "id", + "value" + ], + "type": "object" + }, "StateDelta": { "description": "Application state delta.", "items": { @@ -3340,6 +3371,13 @@ }, "type": "array" }, + "logs": { + "description": "\\[lg\\] Logs for the application being executed by this transaction.", + "items": { + "$ref": "#/components/schemas/LogItem" + }, + "type": "array" + }, "pool-error": { "description": "Indicates that the transaction was kicked out of this node's transaction pool (and specifies why that happened). An empty string indicates the transaction wasn't kicked out of this node's txpool due to an error.\n", "type": "string" @@ -3404,6 +3442,13 @@ }, "type": "array" }, + "logs": { + "description": "\\[lg\\] Logs for the application being executed by this transaction.", + "items": { + "$ref": "#/components/schemas/LogItem" + }, + "type": "array" + }, "pool-error": { "description": "Indicates that the transaction was kicked out of this node's transaction pool (and specifies why that happened). An empty string indicates the transaction wasn't kicked out of this node's txpool due to an error.\n", "type": "string" diff --git a/daemon/algod/api/client/restClient.go b/daemon/algod/api/client/restClient.go index a00f25880e..beab935e7e 100644 --- a/daemon/algod/api/client/restClient.go +++ b/daemon/algod/api/client/restClient.go @@ -434,8 +434,8 @@ func (client RestClient) PendingTransactionInformation(transactionID string) (re return } -// PendingTransactionInformationV2 gets information about a recently issued -// transaction. See PendingTransactionInformation for more details. +// PendingTransactionInformationV2 gets information about a recently issued transaction. +// See PendingTransactionInformation for more details. func (client RestClient) PendingTransactionInformationV2(transactionID string) (response generatedV2.PendingTransactionResponse, err error) { transactionID = stripTransaction(transactionID) err = client.get(&response, fmt.Sprintf("/v2/transactions/pending/%s", transactionID), nil) diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index 4e0b21581c..5a76d6fd2e 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -521,6 +521,13 @@ func doDryrunRequest(dr *DryrunRequest, response *generated.DryrunResponse) { } result.LocalDeltas = &localDeltas } + + var err3 error + result.Logs, err3 = DeltaLogToLog(delta.Logs, appIdx) + if err3 != nil { + messages = append(messages, err3.Error()) + } + if pass { messages = append(messages, "PASS") } else { @@ -563,6 +570,22 @@ func StateDeltaToStateDelta(sd basics.StateDelta) *generated.StateDelta { return &gsd } +// DeltaLogToLog EvalDelta.Logs to generated.LogItem +func DeltaLogToLog(logs []basics.LogItem, appIdx basics.AppIndex) (*[]generated.LogItem, error) { + if len(logs) == 0 { + return nil, nil + } + encodedLogs := make([]generated.LogItem, 0, len(logs)) + for _, log := range logs { + if log.ID != 0 { + return nil, fmt.Errorf("logging for a foreign app is not supported") + } + msg := base64.StdEncoding.EncodeToString([]byte(log.Message)) + encodedLogs = append(encodedLogs, generated.LogItem{Id: uint64(appIdx), Value: msg}) + } + return &encodedLogs, nil +} + // MergeAppParams merges values, existing in "base" take priority over new in "update" func MergeAppParams(base *basics.AppParams, update *basics.AppParams) { if len(base.ApprovalProgram) == 0 && len(update.ApprovalProgram) > 0 { diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go index 421025dab1..c926934dd6 100644 --- a/daemon/algod/api/server/v2/dryrun_test.go +++ b/daemon/algod/api/server/v2/dryrun_test.go @@ -346,7 +346,7 @@ func init() { // legder requires proto string and proto params set var proto config.ConsensusParams - proto.LogicSigVersion = 4 + proto.LogicSigVersion = 5 proto.LogicSigMaxCost = 20000 proto.MaxAppProgramCost = 700 proto.MaxAppKeyLen = 64 @@ -1091,3 +1091,113 @@ int 1`) logResponse(t, &response) } } + +func TestDryrunLogs(t *testing.T) { + t.Parallel() + + ops, err := logic.AssembleString(` +#pragma version 5 +byte "A" +loop: +int 0 +dup2 +getbyte +int 1 ++ +dup +int 97 //ascii code of last char +<= +bz end +setbyte +dup +log +b loop +end: +int 1 +return +`) + + require.NoError(t, err) + approval := ops.Program + ops, err = logic.AssembleString("int 1") + clst := ops.Program + ops, err = logic.AssembleString("#pragma version 5 \nint 1") + approv := ops.Program + require.NoError(t, err) + + var appIdx basics.AppIndex = 1 + creator := randomAddress() + sender := randomAddress() + dr := DryrunRequest{ + Txns: []transactions.SignedTxn{ + { + Txn: transactions.Transaction{ + Header: transactions.Header{Sender: sender}, + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: transactions.OptInOC, + }, + }, + }, + { + Txn: transactions.Transaction{ + Header: transactions.Header{Sender: sender}, + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx + 1, + OnCompletion: transactions.OptInOC, + }, + }, + }, + }, + Apps: []generated.Application{ + { + Id: uint64(appIdx), + Params: generated.ApplicationParams{ + Creator: creator.String(), + ApprovalProgram: approval, + ClearStateProgram: clst, + LocalStateSchema: &generated.ApplicationStateSchema{NumByteSlice: 1}, + }, + }, + { + Id: uint64(appIdx + 1), + Params: generated.ApplicationParams{ + Creator: creator.String(), + ApprovalProgram: approv, + ClearStateProgram: clst, + LocalStateSchema: &generated.ApplicationStateSchema{NumByteSlice: 1}, + }, + }, + }, + Accounts: []generated.Account{ + { + Address: sender.String(), + Status: "Online", + Amount: 10000000, + }, + }, + } + dr.ProtocolVersion = string(dryrunProtoVersion) + + var response generated.DryrunResponse + doDryrunRequest(&dr, &response) + require.NoError(t, err) + checkAppCallPass(t, &response) + if t.Failed() { + logResponse(t, &response) + } + logs := *response.Txns[0].Logs + assert.Equal(t, 32, len(logs)) + for i, m := range logs { + assert.Equal(t, base64.StdEncoding.EncodeToString([]byte(string(rune('B'+i)))), m.Value) + } + encoded := string(protocol.EncodeJSON(response.Txns[0])) + assert.Contains(t, encoded, "logs") + + assert.Empty(t, response.Txns[1].Logs) + encoded = string(protocol.EncodeJSON(response.Txns[1])) + assert.NotContains(t, encoded, "logs") + +} diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index 73c3e0faa4..6cebc1644a 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -236,136 +236,137 @@ func RegisterHandlers(router interface { var swaggerSpec = []string{ "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", - "RT/e+7n3LbPnR0f/xb7K9PKWK95pz3buf5GaLn/iGfOxJZr72deb+1TSay5UaMwq7Jvp5JuvufpTiSLP", - "c0Y9g6yrIev/Ii+lupa+J56udVHwauu3se4oBf/JDdLhfKWpMG8lrvBS/5kqP8eCeSPKhT5/dWvlQt/0", - "+qdy+VrK5ffxsbPnt9zgv/8V/1Od/t7U6ZlVd4erU2fK2fSFuS1T2Vp4/mX08Llw15od08nuqsMek59U", - "wvUTlwJhwUaenjfhZpVZn4gvY+ZT9YLPWnR19kcHtFPl4EfY6n0K/HwN7FcHPhHZr5RmTcGHKVMV+5Xn", - "efAblaPyZvssru/b58h7v2bcbtAYWksAn/RNOV2uujceZJfgH65bGnQClMOYflv0cgmjX7S3tQFDDeZE", - "8NnR0VEsGaiPs/PfWIwpyf5aJTlcQT5k9RgSvffru77/PPqFrGHZgfDeHZE6qvi+gLYSwejnsLtv6W+D", - "3RslHxl2zYX7yEhQu8p+Mq0Qxn8p3iYJuZTU5oyIf108QZAxXNp3MPc9vH9/1bpvdig7va5Npq7luOKi", - "V3w8d2nwlJjeuBuMYh5Ao6lmzH/6N9/6b9czTulKqjatPwgH+5I0vY8SNEXTVkLSBLTLaRb73oMHecPu", - "E1VDJXjmMHtvv+jV03vRL2tbHOP7Prbp7ytLQ0NjJ698CaPO33MUeTRX7RcLE6LQ0KVhgOdzl6jS+9WG", - "k4Mfux8eiPw6b55QRhv7jppYq/Oj+E6thzT0OBKnGl/jp89IcMpKd0xsHWjH8zmFcNdKm/kEFU7XuRY2", - "fm5o/MVz3tP65vPN/w8AAP//J+D4wN2NAAA=", + "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", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/private/types.go b/daemon/algod/api/server/v2/generated/private/types.go index ac0f2e6709..b436e9c902 100644 --- a/daemon/algod/api/server/v2/generated/private/types.go +++ b/daemon/algod/api/server/v2/generated/private/types.go @@ -307,6 +307,7 @@ type DryrunTxnResult struct { LocalDeltas *[]AccountStateDelta `json:"local-deltas,omitempty"` LogicSigMessages *[]string `json:"logic-sig-messages,omitempty"` LogicSigTrace *[]DryrunState `json:"logic-sig-trace,omitempty"` + Logs *[]LogItem `json:"logs,omitempty"` } // ErrorResponse defines model for ErrorResponse. @@ -336,6 +337,16 @@ type EvalDeltaKeyValue struct { Value EvalDelta `json:"value"` } +// LogItem defines model for LogItem. +type LogItem struct { + + // unique application identifier + Id uint64 `json:"id"` + + // base64 encoded log message + Value string `json:"value"` +} + // StateDelta defines model for StateDelta. type StateDelta []EvalDeltaKeyValue @@ -565,6 +576,9 @@ type PendingTransactionResponse struct { // \[ld\] Local state key/value changes for the application being executed by this transaction. LocalStateDelta *[]AccountStateDelta `json:"local-state-delta,omitempty"` + // \[lg\] Logs for the application being executed by this transaction. + Logs *[]LogItem `json:"logs,omitempty"` + // Indicates that the transaction was kicked out of this node's transaction pool (and specifies why that happened). An empty string indicates the transaction wasn't kicked out of this node's txpool due to an error. PoolError string `json:"pool-error"` diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index 952a49eb41..3b870f435d 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -657,140 +657,141 @@ var swaggerSpec = []string{ "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", - "9wAA", + "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", + "S8kuMdP/++v/HwAA//96ww9J5PoAAA==", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/types.go b/daemon/algod/api/server/v2/generated/types.go index f26919b699..08c8d0f2ba 100644 --- a/daemon/algod/api/server/v2/generated/types.go +++ b/daemon/algod/api/server/v2/generated/types.go @@ -307,6 +307,7 @@ type DryrunTxnResult struct { LocalDeltas *[]AccountStateDelta `json:"local-deltas,omitempty"` LogicSigMessages *[]string `json:"logic-sig-messages,omitempty"` LogicSigTrace *[]DryrunState `json:"logic-sig-trace,omitempty"` + Logs *[]LogItem `json:"logs,omitempty"` } // ErrorResponse defines model for ErrorResponse. @@ -336,6 +337,16 @@ type EvalDeltaKeyValue struct { Value EvalDelta `json:"value"` } +// LogItem defines model for LogItem. +type LogItem struct { + + // unique application identifier + Id uint64 `json:"id"` + + // base64 encoded log message + Value string `json:"value"` +} + // StateDelta defines model for StateDelta. type StateDelta []EvalDeltaKeyValue @@ -565,6 +576,9 @@ type PendingTransactionResponse struct { // \[ld\] Local state key/value changes for the application being executed by this transaction. LocalStateDelta *[]AccountStateDelta `json:"local-state-delta,omitempty"` + // \[lg\] Logs for the application being executed by this transaction. + Logs *[]LogItem `json:"logs,omitempty"` + // Indicates that the transaction was kicked out of this node's transaction pool (and specifies why that happened). An empty string indicates the transaction wasn't kicked out of this node's txpool due to an error. PoolError string `json:"pool-error"` diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index 070baff870..7a60425444 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -513,6 +513,7 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, ReceiverRewards *uint64 `codec:"receiver-rewards,omitempty"` SenderRewards *uint64 `codec:"sender-rewards,omitempty"` Txn transactions.SignedTxn `codec:"txn"` + Logs *[]generated.LogItem `codec:"logs,omitempty"` }{ Txn: txn.Txn, } @@ -531,13 +532,15 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, response.SenderRewards = &txn.ApplyData.SenderRewards.Raw response.ReceiverRewards = &txn.ApplyData.ReceiverRewards.Raw response.CloseRewards = &txn.ApplyData.CloseRewards.Raw - response.AssetIndex = computeAssetIndexFromTxn(txn, v2.Node.Ledger()) response.ApplicationIndex = computeAppIndexFromTxn(txn, v2.Node.Ledger()) - response.LocalStateDelta, response.GlobalStateDelta = convertToDeltas(txn) - } + response.Logs, err = convertToLogItems(txn, response.ApplicationIndex) + if err != nil { + return internalError(ctx, err, err.Error(), v2.Log) + } + } data, err := encode(handle, response) if err != nil { return internalError(ctx, err, errFailedToEncodeResponse, v2.Log) diff --git a/daemon/algod/api/server/v2/utils.go b/daemon/algod/api/server/v2/utils.go index e4b2dd24c4..a3bfa12517 100644 --- a/daemon/algod/api/server/v2/utils.go +++ b/daemon/algod/api/server/v2/utils.go @@ -266,6 +266,35 @@ func convertToDeltas(txn node.TxnWithStatus) (*[]generated.AccountStateDelta, *g return localStateDelta, stateDeltaToStateDelta(txn.ApplyData.EvalDelta.GlobalDelta) } +func convertToLogItems(txn node.TxnWithStatus, aidx *uint64) (*[]generated.LogItem, error) { + var logItems *[]generated.LogItem + if len(txn.ApplyData.EvalDelta.Logs) > 0 { + l := make([]generated.LogItem, 0, len(txn.ApplyData.EvalDelta.Logs)) + + for _, v := range txn.ApplyData.EvalDelta.Logs { + // Resolve appid from index + var appid uint64 + if v.ID != 0 { + return nil, fmt.Errorf("logging for a foreign app is not supported") + } else if txn.Txn.Txn.ApplicationID == 0 { + if aidx == nil { + return nil, fmt.Errorf("app index cannot be nil") + } + appid = *aidx + } else { + appid = uint64(txn.Txn.Txn.ApplicationID) + } + l = append(l, generated.LogItem{ + Id: appid, + Value: base64.StdEncoding.EncodeToString([]byte(v.Message)), + }) + } + + logItems = &l + } + return logItems, nil +} + // printableUTF8OrEmpty checks to see if the entire string is a UTF8 printable string. // If this is the case, the string is returned as is. Otherwise, the empty string is returned. func printableUTF8OrEmpty(in string) string { diff --git a/data/basics/msgp_gen.go b/data/basics/msgp_gen.go index 463f0bf143..6127920e8b 100644 --- a/data/basics/msgp_gen.go +++ b/data/basics/msgp_gen.go @@ -115,6 +115,14 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // +// LogItem +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// // Round // |-----> MarshalMsg // |-----> CanMarshalMsg @@ -4129,20 +4137,24 @@ func (z DeltaAction) MsgIsZero() bool { func (z *EvalDelta) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0007Len := uint32(2) - var zb0007Mask uint8 /* 3 bits */ + zb0008Len := uint32(3) + var zb0008Mask uint8 /* 4 bits */ if len((*z).GlobalDelta) == 0 { - zb0007Len-- - zb0007Mask |= 0x2 + zb0008Len-- + zb0008Mask |= 0x2 } if len((*z).LocalDeltas) == 0 { - zb0007Len-- - zb0007Mask |= 0x4 + zb0008Len-- + zb0008Mask |= 0x4 + } + if len((*z).Logs) == 0 { + zb0008Len-- + zb0008Mask |= 0x8 } - // variable map header, size zb0007Len - o = append(o, 0x80|uint8(zb0007Len)) - if zb0007Len != 0 { - if (zb0007Mask & 0x2) == 0 { // if not empty + // variable map header, size zb0008Len + o = append(o, 0x80|uint8(zb0008Len)) + if zb0008Len != 0 { + if (zb0008Mask & 0x2) == 0 { // if not empty // string "gd" o = append(o, 0xa2, 0x67, 0x64) if (*z).GlobalDelta == nil { @@ -4162,7 +4174,7 @@ func (z *EvalDelta) MarshalMsg(b []byte) (o []byte) { o = zb0002.MarshalMsg(o) } } - if (zb0007Mask & 0x4) == 0 { // if not empty + if (zb0008Mask & 0x4) == 0 { // if not empty // string "ld" o = append(o, 0xa2, 0x6c, 0x64) if (*z).LocalDeltas == nil { @@ -4197,6 +4209,40 @@ func (z *EvalDelta) MarshalMsg(b []byte) (o []byte) { } } } + if (zb0008Mask & 0x8) == 0 { // if not empty + // string "lg" + o = append(o, 0xa2, 0x6c, 0x67) + if (*z).Logs == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).Logs))) + } + for zb0007 := range (*z).Logs { + // omitempty: check for empty values + zb0009Len := uint32(2) + var zb0009Mask uint8 /* 3 bits */ + if (*z).Logs[zb0007].ID == 0 { + zb0009Len-- + zb0009Mask |= 0x2 + } + if (*z).Logs[zb0007].Message == "" { + zb0009Len-- + zb0009Mask |= 0x4 + } + // variable map header, size zb0009Len + o = append(o, 0x80|uint8(zb0009Len)) + if (zb0009Mask & 0x2) == 0 { // if not empty + // string "i" + o = append(o, 0xa1, 0x69) + o = msgp.AppendUint64(o, (*z).Logs[zb0007].ID) + } + if (zb0009Mask & 0x4) == 0 { // if not empty + // string "m" + o = append(o, 0xa1, 0x6d) + o = msgp.AppendString(o, (*z).Logs[zb0007].Message) + } + } + } } return } @@ -4210,38 +4256,38 @@ func (_ *EvalDelta) CanMarshalMsg(z interface{}) bool { func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { var field []byte _ = field - var zb0007 int - var zb0008 bool - zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0008 int + var zb0009 bool + zb0008, zb0009, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0008, zb0009, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0007 > 0 { - zb0007-- - var zb0009 int - var zb0010 bool - zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) + if zb0008 > 0 { + zb0008-- + var zb0010 int + var zb0011 bool + zb0010, zb0011, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalDelta") return } - if zb0009 > config.MaxStateDeltaKeys { - err = msgp.ErrOverflow(uint64(zb0009), uint64(config.MaxStateDeltaKeys)) + if zb0010 > config.MaxStateDeltaKeys { + err = msgp.ErrOverflow(uint64(zb0010), uint64(config.MaxStateDeltaKeys)) err = msgp.WrapError(err, "struct-from-array", "GlobalDelta") return } - if zb0010 { + if zb0011 { (*z).GlobalDelta = nil } else if (*z).GlobalDelta == nil { - (*z).GlobalDelta = make(StateDelta, zb0009) + (*z).GlobalDelta = make(StateDelta, zb0010) } - for zb0009 > 0 { + for zb0010 > 0 { var zb0001 string var zb0002 ValueDelta - zb0009-- + zb0010-- zb0001, bts, err = msgp.ReadStringBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalDelta") @@ -4255,55 +4301,55 @@ func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { (*z).GlobalDelta[zb0001] = zb0002 } } - if zb0007 > 0 { - zb0007-- - var zb0011 int - var zb0012 bool - zb0011, zb0012, bts, err = msgp.ReadMapHeaderBytes(bts) + if zb0008 > 0 { + zb0008-- + var zb0012 int + var zb0013 bool + zb0012, zb0013, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalDeltas") return } - if zb0011 > config.MaxEvalDeltaAccounts { - err = msgp.ErrOverflow(uint64(zb0011), uint64(config.MaxEvalDeltaAccounts)) + if zb0012 > config.MaxEvalDeltaAccounts { + err = msgp.ErrOverflow(uint64(zb0012), uint64(config.MaxEvalDeltaAccounts)) err = msgp.WrapError(err, "struct-from-array", "LocalDeltas") return } - if zb0012 { + if zb0013 { (*z).LocalDeltas = nil } else if (*z).LocalDeltas == nil { - (*z).LocalDeltas = make(map[uint64]StateDelta, zb0011) + (*z).LocalDeltas = make(map[uint64]StateDelta, zb0012) } - for zb0011 > 0 { + for zb0012 > 0 { var zb0003 uint64 var zb0004 StateDelta - zb0011-- + zb0012-- zb0003, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalDeltas") return } - var zb0013 int - var zb0014 bool - zb0013, zb0014, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0014 int + var zb0015 bool + zb0014, zb0015, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalDeltas", zb0003) return } - if zb0013 > config.MaxStateDeltaKeys { - err = msgp.ErrOverflow(uint64(zb0013), uint64(config.MaxStateDeltaKeys)) + if zb0014 > config.MaxStateDeltaKeys { + err = msgp.ErrOverflow(uint64(zb0014), uint64(config.MaxStateDeltaKeys)) err = msgp.WrapError(err, "struct-from-array", "LocalDeltas", zb0003) return } - if zb0014 { + if zb0015 { zb0004 = nil } else if zb0004 == nil { - zb0004 = make(StateDelta, zb0013) + zb0004 = make(StateDelta, zb0014) } - for zb0013 > 0 { + for zb0014 > 0 { var zb0005 string var zb0006 ValueDelta - zb0013-- + zb0014-- zb0005, bts, err = msgp.ReadStringBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalDeltas", zb0003) @@ -4319,8 +4365,101 @@ func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { (*z).LocalDeltas[zb0003] = zb0004 } } - if zb0007 > 0 { - err = msgp.ErrTooManyArrayFields(zb0007) + if zb0008 > 0 { + zb0008-- + var zb0016 int + var zb0017 bool + zb0016, zb0017, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs") + return + } + if zb0016 > config.MaxLogCalls { + err = msgp.ErrOverflow(uint64(zb0016), uint64(config.MaxLogCalls)) + err = msgp.WrapError(err, "struct-from-array", "Logs") + return + } + if zb0017 { + (*z).Logs = nil + } else if (*z).Logs != nil && cap((*z).Logs) >= zb0016 { + (*z).Logs = ((*z).Logs)[:zb0016] + } else { + (*z).Logs = make([]LogItem, zb0016) + } + for zb0007 := range (*z).Logs { + var zb0018 int + var zb0019 bool + zb0018, zb0019, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0018, zb0019, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs", zb0007) + return + } + if zb0018 > 0 { + zb0018-- + (*z).Logs[zb0007].ID, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs", zb0007, "struct-from-array", "ID") + return + } + } + if zb0018 > 0 { + zb0018-- + (*z).Logs[zb0007].Message, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs", zb0007, "struct-from-array", "Message") + return + } + } + if zb0018 > 0 { + err = msgp.ErrTooManyArrayFields(zb0018) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs", zb0007, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs", zb0007) + return + } + if zb0019 { + (*z).Logs[zb0007] = LogItem{} + } + for zb0018 > 0 { + zb0018-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs", zb0007) + return + } + switch string(field) { + case "i": + (*z).Logs[zb0007].ID, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs", zb0007, "ID") + return + } + case "m": + (*z).Logs[zb0007].Message, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs", zb0007, "Message") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logs", zb0007) + return + } + } + } + } + } + } + if zb0008 > 0 { + err = msgp.ErrTooManyArrayFields(zb0008) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -4331,11 +4470,11 @@ func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0008 { + if zb0009 { (*z) = EvalDelta{} } - for zb0007 > 0 { - zb0007-- + for zb0008 > 0 { + zb0008-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) @@ -4343,27 +4482,27 @@ func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { } switch string(field) { case "gd": - var zb0015 int - var zb0016 bool - zb0015, zb0016, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0020 int + var zb0021 bool + zb0020, zb0021, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalDelta") return } - if zb0015 > config.MaxStateDeltaKeys { - err = msgp.ErrOverflow(uint64(zb0015), uint64(config.MaxStateDeltaKeys)) + if zb0020 > config.MaxStateDeltaKeys { + err = msgp.ErrOverflow(uint64(zb0020), uint64(config.MaxStateDeltaKeys)) err = msgp.WrapError(err, "GlobalDelta") return } - if zb0016 { + if zb0021 { (*z).GlobalDelta = nil } else if (*z).GlobalDelta == nil { - (*z).GlobalDelta = make(StateDelta, zb0015) + (*z).GlobalDelta = make(StateDelta, zb0020) } - for zb0015 > 0 { + for zb0020 > 0 { var zb0001 string var zb0002 ValueDelta - zb0015-- + zb0020-- zb0001, bts, err = msgp.ReadStringBytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalDelta") @@ -4377,53 +4516,53 @@ func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { (*z).GlobalDelta[zb0001] = zb0002 } case "ld": - var zb0017 int - var zb0018 bool - zb0017, zb0018, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0022 int + var zb0023 bool + zb0022, zb0023, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "LocalDeltas") return } - if zb0017 > config.MaxEvalDeltaAccounts { - err = msgp.ErrOverflow(uint64(zb0017), uint64(config.MaxEvalDeltaAccounts)) + if zb0022 > config.MaxEvalDeltaAccounts { + err = msgp.ErrOverflow(uint64(zb0022), uint64(config.MaxEvalDeltaAccounts)) err = msgp.WrapError(err, "LocalDeltas") return } - if zb0018 { + if zb0023 { (*z).LocalDeltas = nil } else if (*z).LocalDeltas == nil { - (*z).LocalDeltas = make(map[uint64]StateDelta, zb0017) + (*z).LocalDeltas = make(map[uint64]StateDelta, zb0022) } - for zb0017 > 0 { + for zb0022 > 0 { var zb0003 uint64 var zb0004 StateDelta - zb0017-- + zb0022-- zb0003, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "LocalDeltas") return } - var zb0019 int - var zb0020 bool - zb0019, zb0020, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0024 int + var zb0025 bool + zb0024, zb0025, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "LocalDeltas", zb0003) return } - if zb0019 > config.MaxStateDeltaKeys { - err = msgp.ErrOverflow(uint64(zb0019), uint64(config.MaxStateDeltaKeys)) + if zb0024 > config.MaxStateDeltaKeys { + err = msgp.ErrOverflow(uint64(zb0024), uint64(config.MaxStateDeltaKeys)) err = msgp.WrapError(err, "LocalDeltas", zb0003) return } - if zb0020 { + if zb0025 { zb0004 = nil } else if zb0004 == nil { - zb0004 = make(StateDelta, zb0019) + zb0004 = make(StateDelta, zb0024) } - for zb0019 > 0 { + for zb0024 > 0 { var zb0005 string var zb0006 ValueDelta - zb0019-- + zb0024-- zb0005, bts, err = msgp.ReadStringBytes(bts) if err != nil { err = msgp.WrapError(err, "LocalDeltas", zb0003) @@ -4438,6 +4577,97 @@ func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { } (*z).LocalDeltas[zb0003] = zb0004 } + case "lg": + var zb0026 int + var zb0027 bool + zb0026, zb0027, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Logs") + return + } + if zb0026 > config.MaxLogCalls { + err = msgp.ErrOverflow(uint64(zb0026), uint64(config.MaxLogCalls)) + err = msgp.WrapError(err, "Logs") + return + } + if zb0027 { + (*z).Logs = nil + } else if (*z).Logs != nil && cap((*z).Logs) >= zb0026 { + (*z).Logs = ((*z).Logs)[:zb0026] + } else { + (*z).Logs = make([]LogItem, zb0026) + } + for zb0007 := range (*z).Logs { + var zb0028 int + var zb0029 bool + zb0028, zb0029, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0028, zb0029, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Logs", zb0007) + return + } + if zb0028 > 0 { + zb0028-- + (*z).Logs[zb0007].ID, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Logs", zb0007, "struct-from-array", "ID") + return + } + } + if zb0028 > 0 { + zb0028-- + (*z).Logs[zb0007].Message, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Logs", zb0007, "struct-from-array", "Message") + return + } + } + if zb0028 > 0 { + err = msgp.ErrTooManyArrayFields(zb0028) + if err != nil { + err = msgp.WrapError(err, "Logs", zb0007, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "Logs", zb0007) + return + } + if zb0029 { + (*z).Logs[zb0007] = LogItem{} + } + for zb0028 > 0 { + zb0028-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "Logs", zb0007) + return + } + switch string(field) { + case "i": + (*z).Logs[zb0007].ID, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Logs", zb0007, "ID") + return + } + case "m": + (*z).Logs[zb0007].Message, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Logs", zb0007, "Message") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "Logs", zb0007) + return + } + } + } + } + } default: err = msgp.ErrNoField(string(field)) if err != nil { @@ -4481,12 +4711,145 @@ func (z *EvalDelta) Msgsize() (s int) { } } } + s += 3 + msgp.ArrayHeaderSize + for zb0007 := range (*z).Logs { + s += 1 + 2 + msgp.Uint64Size + 2 + msgp.StringPrefixSize + len((*z).Logs[zb0007].Message) + } return } // MsgIsZero returns whether this is a zero value func (z *EvalDelta) MsgIsZero() bool { - return (len((*z).GlobalDelta) == 0) && (len((*z).LocalDeltas) == 0) + return (len((*z).GlobalDelta) == 0) && (len((*z).LocalDeltas) == 0) && (len((*z).Logs) == 0) +} + +// MarshalMsg implements msgp.Marshaler +func (z *LogItem) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(2) + var zb0001Mask uint8 /* 3 bits */ + if (*z).ID == 0 { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).Message == "" { + zb0001Len-- + zb0001Mask |= 0x4 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "i" + o = append(o, 0xa1, 0x69) + o = msgp.AppendUint64(o, (*z).ID) + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "m" + o = append(o, 0xa1, 0x6d) + o = msgp.AppendString(o, (*z).Message) + } + } + return +} + +func (_ *LogItem) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*LogItem) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *LogItem) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 int + var zb0002 bool + zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 > 0 { + zb0001-- + (*z).ID, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ID") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).Message, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Message") + return + } + } + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 { + (*z) = LogItem{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "i": + (*z).ID, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ID") + return + } + case "m": + (*z).Message, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Message") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *LogItem) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*LogItem) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *LogItem) Msgsize() (s int) { + s = 1 + 2 + msgp.Uint64Size + 2 + msgp.StringPrefixSize + len((*z).Message) + return +} + +// MsgIsZero returns whether this is a zero value +func (z *LogItem) MsgIsZero() bool { + return ((*z).ID == 0) && ((*z).Message == "") } // MarshalMsg implements msgp.Marshaler diff --git a/data/basics/msgp_gen_test.go b/data/basics/msgp_gen_test.go index 558e1c0911..c0e43a4fd4 100644 --- a/data/basics/msgp_gen_test.go +++ b/data/basics/msgp_gen_test.go @@ -432,6 +432,66 @@ func BenchmarkUnmarshalEvalDelta(b *testing.B) { } } +func TestMarshalUnmarshalLogItem(t *testing.T) { + partitiontest.PartitionTest(t) + v := LogItem{} + bts := v.MarshalMsg(nil) + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingLogItem(t *testing.T) { + protocol.RunEncodingTest(t, &LogItem{}) +} + +func BenchmarkMarshalMsgLogItem(b *testing.B) { + v := LogItem{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgLogItem(b *testing.B) { + v := LogItem{} + bts := make([]byte, 0, v.Msgsize()) + bts = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalLogItem(b *testing.B) { + v := LogItem{} + bts := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + func TestMarshalUnmarshalStateDelta(t *testing.T) { partitiontest.PartitionTest(t) v := StateDelta{} diff --git a/data/basics/teal.go b/data/basics/teal.go index e95aaf2dbe..7dab26b83b 100644 --- a/data/basics/teal.go +++ b/data/basics/teal.go @@ -123,6 +123,22 @@ func (sd StateDelta) Valid(proto *config.ConsensusParams) error { return nil } +// LogItem is contains logs for an application +// ID is the offset into Txn.ForeignApps +type LogItem struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + ID uint64 `codec:"i"` + Message string `codec:"m"` +} + +// Equal checks whether two LogItems are equal. +func (l LogItem) Equal(o LogItem) bool { + + return l.ID == o.ID && l.Message == o.Message + +} + // EvalDelta stores StateDeltas for an application's global key/value store, as // well as StateDeltas for some number of accounts holding local state for that // application @@ -134,6 +150,8 @@ type EvalDelta struct { // When decoding EvalDeltas, the integer key represents an offset into // [txn.Sender, txn.Accounts[0], txn.Accounts[1], ...] LocalDeltas map[uint64]StateDelta `codec:"ld,allocbound=config.MaxEvalDeltaAccounts"` + + Logs []LogItem `codec:"lg,allocbound=config.MaxLogCalls"` } // Equal compares two EvalDeltas and returns whether or not they are @@ -165,6 +183,17 @@ func (ed EvalDelta) Equal(o EvalDelta) bool { return false } + // Logs must be equal + if len(ed.Logs) != len(o.Logs) { + return false + } + for i, l := range ed.Logs { + ok := l.Equal(o.Logs[i]) + if !ok { + return false + } + } + return true } diff --git a/data/basics/teal_test.go b/data/basics/teal_test.go index 35b4331e8f..526573aaf5 100644 --- a/data/basics/teal_test.go +++ b/data/basics/teal_test.go @@ -156,12 +156,14 @@ func TestEvalDeltaEqual(t *testing.T) { d2 = EvalDelta{ GlobalDelta: nil, LocalDeltas: nil, + Logs: nil, } a.True(d1.Equal(d2)) d2 = EvalDelta{ GlobalDelta: StateDelta{}, LocalDeltas: map[uint64]StateDelta{}, + Logs: []LogItem{}, } a.True(d1.Equal(d2)) @@ -221,4 +223,34 @@ func TestEvalDeltaEqual(t *testing.T) { }, } a.False(d1.Equal(d2)) + + d2 = EvalDelta{ + Logs: []LogItem{{ID: 0, Message: "val"}}, + } + a.False(d1.Equal(d2)) + + d1 = EvalDelta{ + Logs: []LogItem{{ID: 0, Message: "val2"}}, + } + a.False(d1.Equal(d2)) + + d1 = EvalDelta{ + Logs: []LogItem{{ID: 1, Message: "val"}}, + } + a.False(d1.Equal(d2)) + + d1 = EvalDelta{ + Logs: []LogItem{{ID: 1, Message: "val2"}}, + } + a.False(d1.Equal(d2)) + + d1 = EvalDelta{ + Logs: []LogItem{{ID: 0, Message: "val"}, {ID: 0, Message: "val2"}}, + } + a.False(d1.Equal(d2)) + + d1 = EvalDelta{ + Logs: []LogItem{{ID: 0, Message: "val"}}, + } + a.True(d1.Equal(d2)) } diff --git a/data/transactions/application.go b/data/transactions/application.go index 90dcc81d75..4588ed22f8 100644 --- a/data/transactions/application.go +++ b/data/transactions/application.go @@ -233,3 +233,45 @@ func (ac *ApplicationCallTxnFields) IndexByAddress(target basics.Address, sender return 0, fmt.Errorf("invalid Account reference %s", target) } + +// AppIDByIndex converts an integer index into an application id associated with the +// transaction. Index 0 corresponds to the current app, and an index > 0 +// corresponds to an offset into txn.ForeignApps. Returns an error if the index is +// not valid. +func (ac *ApplicationCallTxnFields) AppIDByIndex(i uint64) (basics.AppIndex, error) { + + // Index 0 always corresponds to the current app + if i == 0 { + return ac.ApplicationID, nil + } + + // An index > 0 corresponds to an offset into txn.ForeignApps. Check to + // make sure the index is valid. + if i > uint64(len(ac.ForeignApps)) { + err := fmt.Errorf("invalid Foreign App reference %d", i) + return basics.AppIndex(0), err + } + + // aidx must be in [1, len(ac.ForeignApps)] + return ac.ForeignApps[i-1], nil +} + +// IndexByAppID converts an application id into an integer offset into [current app, +// txn.ForeignApps[0], ...], returning the index at the first match. It returns +// an error if there is no such match. +func (ac *ApplicationCallTxnFields) IndexByAppID(appID basics.AppIndex) (uint64, error) { + + // Index 0 always corresponds to the current app + if appID == ac.ApplicationID { + return 0, nil + } + + // Otherwise we index into ac.ForeignApps + for i, id := range ac.ForeignApps { + if appID == id { + return uint64(i) + 1, nil + } + } + + return 0, fmt.Errorf("invalid Foreign App reference %d", appID) +} diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go index 78670bf18a..5d50d71266 100644 --- a/data/transactions/application_test.go +++ b/data/transactions/application_test.go @@ -117,3 +117,30 @@ func TestEncodedAppTxnAllocationBounds(t *testing.T) { } } } + +func TestIDByIndex(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + ac := ApplicationCallTxnFields{} + ac.ApplicationID = 1 + appID, err := ac.AppIDByIndex(0) + a.NoError(err) + a.Equal(basics.AppIndex(1), appID) + appID, err = ac.AppIDByIndex(1) + a.Contains(err.Error(), "invalid Foreign App reference") + +} + +func TestIndexByID(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + ac := ApplicationCallTxnFields{} + ac.ApplicationID = 1 + aidx, err := ac.IndexByAppID(1) + a.NoError(err) + a.Equal(uint64(0), aidx) + aidx, err = ac.IndexByAppID(2) + a.Contains(err.Error(), "invalid Foreign App reference") +} diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index f6a219f1c5..c2344409cd 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -386,6 +386,7 @@ App fields used in the `app_params_get` opcode. | `asset_holding_get i` | read from account A and asset B holding field X (imm arg) => {0 or 1 (top), value} | | `asset_params_get i` | read from asset A params field X (imm arg) => {0 or 1 (top), value} | | `app_params_get i` | read from app A params field X (imm arg) => {0 or 1 (top), value} | +| `log` | write bytes to log state of the current application | # Assembler Syntax diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index b262a80429..0b78ca3458 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -1202,3 +1202,14 @@ bitlen interprets arrays as big-endian integers, unlike setbit/getbit - Pushes: []byte - push a byte-array of length X, containing all zero bytes - LogicSigVersion >= 4 + +## log + +- Opcode: 0xb0 +- Pops: *... stack*, []byte +- Pushes: _None_ +- write bytes to log state of the current application +- LogicSigVersion >= 5 +- Mode: Application + +`log` can be called up to MaxLogCalls times in a program, and log up to a total of 1k bytes. diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index c7d18c150e..725dba3b55 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -309,6 +309,7 @@ int 0 extract32bits int 0 extract16bits +log ` var nonsense = map[uint64]string{ @@ -324,7 +325,7 @@ var compiled = map[uint64]string{ 2: "022008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f", 3: "032008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f4478222105531421055427042106552105082106564c4d4b02210538212106391c0081e80780046a6f686e", 4: "042004010200b7a60c26040242420c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482a50512a632223524100034200004322602261222b634848222862482864286548482228236628226724286828692422700048482471004848361c0037001a0031183119311b311d311e311f312024221e312131223123312431253126312731283129312a312b312c312d312e312f44782522531422542b2355220823564c4d4b0222382123391c0081e80780046a6f686e2281d00f24231f880003420001892223902291922394239593a0a1a2a3a4a5a6a7a8a9aaabacadae23af3a00003b003c003d8164", - 5: "052004010200b7a60c26040242420c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482a50512a632223524100034200004322602261222b634848222862482864286548482228236628226724286828692422700048482471004848361c0037001a0031183119311b311d311e311f312024221e312131223123312431253126312731283129312a312b312c312d312e312f44782522531422542b2355220823564c4d4b0222382123391c0081e80780046a6f686e2281d00f24231f880003420001892223902291922394239593a0a1a2a3a4a5a6a7a8a9aaabacadae23af3a00003b003c003d816472064e014f0180070123456789abcd57000824810858245b245a2459", + 5: "052004010200b7a60c26040242420c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482a50512a632223524100034200004322602261222b634848222862482864286548482228236628226724286828692422700048482471004848361c0037001a0031183119311b311d311e311f312024221e312131223123312431253126312731283129312a312b312c312d312e312f44782522531422542b2355220823564c4d4b0222382123391c0081e80780046a6f686e2281d00f24231f880003420001892223902291922394239593a0a1a2a3a4a5a6a7a8a9aaabacadae23af3a00003b003c003d816472064e014f0180070123456789abcd57000824810858245b245a2459b0", } func pseudoOp(opcode string) bool { diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index e8e784eb36..cb1c323f35 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -150,6 +150,8 @@ var opDocByName = map[string]string{ "b&": "A bitwise-and B, where A and B are byte-arrays, zero-left extended to the greater of their lengths", "b^": "A bitwise-xor B, where A and B are byte-arrays, zero-left extended to the greater of their lengths", "b~": "X with all bits inverted", + + "log": "write bytes to log state of the current application", } // OpDoc returns a description of the op @@ -236,6 +238,7 @@ var opDocExtras = map[string]string{ "asset_holding_get": "params: Txn.Accounts offset (or, since v4, an account address that appears in Txn.Accounts or is Txn.Sender), asset id (or, since v4, a Txn.ForeignAssets offset). Return: did_exist flag (1 if exist and 0 otherwise), value.", "asset_params_get": "params: Before v4, Txn.ForeignAssets offset. Since v4, Txn.ForeignAssets offset or an asset id that appears in Txn.ForeignAssets. Return: did_exist flag (1 if exist and 0 otherwise), value.", "app_params_get": "params: Txn.ForeignApps offset or an app id that appears in Txn.ForeignApps. Return: did_exist flag (1 if exist and 0 otherwise), value.", + "log": "`log` can be called up to MaxLogCalls times in a program, and log up to a total of 1k bytes.", } // OpDocExtra returns extra documentation text about an op @@ -251,7 +254,7 @@ var OpGroups = map[string][]string{ "Byteslice Logic": {"b|", "b&", "b^", "b~"}, "Loading Values": {"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "pushint", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "pushbytes", "bzero", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "txn", "gtxn", "txna", "gtxna", "gtxns", "gtxnsa", "global", "load", "store", "gload", "gloads", "gaid", "gaids"}, "Flow Control": {"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2", "dig", "cover", "uncover", "swap", "select", "assert", "callsub", "retsub"}, - "State Access": {"balance", "min_balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get", "app_params_get"}, + "State Access": {"balance", "min_balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get", "app_params_get", "log"}, } // OpCost indicates the cost of an operation over the range of diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index f5937bde18..774ae7c0fe 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -53,6 +53,12 @@ const MaxStringSize = 4096 // MaxByteMathSize is the limit of byte strings supplied as input to byte math opcodes const MaxByteMathSize = 64 +// MaxLogSize is the limit of total log size from n log calls in a program +const MaxLogSize = 1024 + +// MaxLogCalls is the limit of total log calls during a program execution +const MaxLogCalls = config.MaxLogCalls + // stackValue is the type for the operand stack. // Each stackValue is either a valid []byte value or a uint64 value. // If (.Bytes != nil) the stackValue is a []byte value, otherwise uint64 value. @@ -159,6 +165,8 @@ type LedgerForLogic interface { DelGlobal(key string) error GetDelta(txn *transactions.Transaction) (evalDelta basics.EvalDelta, err error) + + AppendLog(txn *transactions.Transaction, value string) error } // EvalSideEffects contains data returned from evaluation @@ -281,7 +289,9 @@ type evalContext struct { version uint64 scratch scratchSpace - cost int // cost incurred so far + cost int // cost incurred so far + logCalls int // number of log calls so far + logSize int // log size of the program so far // Set of PC values that branches we've seen so far might // go. So, if checkStep() skips one, that branch is trying to @@ -3154,3 +3164,26 @@ func opAppParamsGet(cx *evalContext) { cx.stack[last] = value cx.stack = append(cx.stack, stackValue{Uint: exist}) } + +func opLog(cx *evalContext) { + last := len(cx.stack) - 1 + + if cx.logCalls == MaxLogCalls { + cx.err = fmt.Errorf("too many log calls in program. up to %d is allowed", MaxLogCalls) + return + } + cx.logCalls++ + log := cx.stack[last] + cx.logSize += len(log.Bytes) + if cx.logSize > MaxLogSize { + cx.err = fmt.Errorf("program logs too large. %d bytes > %d bytes limit", cx.logSize, MaxLogSize) + return + } + // write log to applyData + err := cx.Ledger.AppendLog(&cx.Txn.Txn, string(log.Bytes)) + if err != nil { + cx.err = err + return + } + cx.stack = cx.stack[:last] +} diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 2cceb2a4f3..de22e2fa22 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -60,6 +60,7 @@ type testLedger struct { appID basics.AppIndex creatorAddr basics.Address mods map[basics.AppIndex]map[string]basics.ValueDelta + logs []basics.LogItem } func makeApp(li uint64, lb uint64, gi uint64, gb uint64) basics.AppParams { @@ -464,9 +465,25 @@ func (l *testLedger) GetDelta(txn *transactions.Transaction) (evalDelta basics.E } } } + evalDelta.Logs = l.logs return } +func (l *testLedger) AppendLog(txn *transactions.Transaction, value string) error { + + appIdx, err := txn.IndexByAppID(l.appID) + if err != nil { + return err + } + _, ok := l.applications[l.appID] + if !ok { + return fmt.Errorf("no such app") + } + + l.logs = append(l.logs, basics.LogItem{ID: appIdx, Message: value}) + return nil +} + func TestEvalModes(t *testing.T) { partitiontest.PartitionTest(t) @@ -590,6 +607,8 @@ asset_params_get AssetTotal pop && != +bytec_0 +log ` type desc struct { source string @@ -712,6 +731,7 @@ pop "int 0\nint 0\nasset_holding_get AssetFrozen", "int 0\nint 0\nasset_params_get AssetManager", "int 0\nint 0\napp_params_get AppApprovalProgram", + "byte 0x01\nlog", } for _, source := range statefulOpcodeCalls { @@ -792,6 +812,7 @@ func testApp(t *testing.T, program string, ep EvalParams, problems ...string) ba require.NoError(t, err) require.Empty(t, delta.GlobalDelta) require.Empty(t, delta.LocalDeltas) + require.Empty(t, delta.Logs) return delta } return basics.EvalDelta{} @@ -2866,3 +2887,45 @@ func TestAppLoop(t *testing.T) { // Infinite loop because multiply by one instead of two testApp(t, stateful+"int 1; loop:; int 1; *; dup; int 10; <; bnz loop; int 16; ==", ep, "dynamic cost") } + +func TestWriteLogs(t *testing.T) { + partitiontest.PartitionTest(t) + + t.Parallel() + + ep := defaultEvalParams(nil, nil) + txn := makeSampleTxn() + txn.Txn.ApplicationID = 100 + ep.Txn = &txn + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + ledger.newApp(txn.Txn.Sender, 100, basics.AppParams{}) + + // write int and bytes values + source := `int 1 +loop: byte "a" +log +int 1 ++ +dup +int 30 +< +bnz loop +` + ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) + require.NoError(t, err) + err = CheckStateful(ops.Program, ep) + require.NoError(t, err) + pass, err := EvalStateful(ops.Program, ep) + require.NoError(t, err) + require.True(t, pass) + delta, err := ledger.GetDelta(&ep.Txn.Txn) + require.NoError(t, err) + require.Empty(t, 0, delta.GlobalDelta) + require.Empty(t, delta.LocalDeltas) + require.Len(t, delta.Logs, 29) +} diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index a6c3feee6f..c6a44d9661 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -4734,3 +4734,129 @@ func TestBytesConversions(t *testing.T) { testAccepts(t, "byte 0x11; byte 0x10; b+; btoi; int 0x21; ==", 4) testAccepts(t, "byte 0x0011; byte 0x10; b+; btoi; int 0x21; ==", 4) } + +func TestLog(t *testing.T) { + partitiontest.PartitionTest(t) + + t.Parallel() + proto := defaultEvalProtoWithVersion(LogicVersion) + txn := transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + }, + } + ledger := makeTestLedger(nil) + ledger.newApp(txn.Txn.Receiver, 0, basics.AppParams{}) + sb := strings.Builder{} + ep := defaultEvalParams(&sb, &txn) + ep.Proto = &proto + ep.Ledger = ledger + testCases := []struct { + source string + loglen int + }{ + { + source: `byte "a logging message"; log; int 1`, + loglen: 1, + }, + { + source: `byte "a logging message"; log; byte "a logging message"; log; int 1`, + loglen: 2, + }, + { + source: fmt.Sprintf(`%s int 1`, strings.Repeat(`byte "a logging message"; log;`, config.MaxLogCalls)), + loglen: MaxLogCalls, + }, + { + source: `int 1; loop: byte "a logging message"; log; int 1; +; dup; int 30; <=; bnz loop;`, + loglen: 30, + }, + { + source: fmt.Sprintf(`byte "%s"; log; int 1`, strings.Repeat("a", MaxLogSize)), + loglen: 1, + }, + } + + //track expected number of logs in ep.Ledger + count := 0 + for i, s := range testCases { + ops := testProg(t, s.source, AssemblerMaxVersion) + + err := CheckStateful(ops.Program, ep) + require.NoError(t, err, s) + + pass, err := EvalStateful(ops.Program, ep) + require.NoError(t, err) + require.True(t, pass) + count += s.loglen + require.Equal(t, len(ledger.logs), count) + if i == len(testCases)-1 { + require.Equal(t, strings.Repeat("a", MaxLogSize), ledger.logs[count-1].Message) + } else { + for _, l := range ledger.logs[count-s.loglen:] { + require.Equal(t, "a logging message", l.Message) + } + } + } + + msg := strings.Repeat("a", 400) + failCases := []struct { + source string + runMode runMode + errContains string + }{ + { + source: fmt.Sprintf(`byte "%s"; log; int 1`, strings.Repeat("a", MaxLogSize+1)), + errContains: fmt.Sprintf("> %d bytes limit", MaxLogSize), + runMode: runModeApplication, + }, + { + source: fmt.Sprintf(`byte "%s"; log; byte "%s"; log; byte "%s"; log; int 1`, msg, msg, msg), + errContains: fmt.Sprintf("> %d bytes limit", MaxLogSize), + runMode: runModeApplication, + }, + { + source: fmt.Sprintf(`%s; int 1`, strings.Repeat(`byte "a"; log;`, config.MaxLogCalls+1)), + errContains: "too many log calls", + runMode: runModeApplication, + }, + { + source: `int 1; loop: byte "a"; log; int 1; +; dup; int 35; <; bnz loop;`, + errContains: "too many log calls", + runMode: runModeApplication, + }, + { + source: fmt.Sprintf(`int 1; loop: byte "%s"; log; int 1; +; dup; int 6; <; bnz loop;`, strings.Repeat(`a`, 400)), + errContains: fmt.Sprintf("> %d bytes limit", MaxLogSize), + runMode: runModeApplication, + }, + { + source: `load 0; log`, + errContains: "log arg 0 wanted []byte but got uint64", + runMode: runModeApplication, + }, + { + source: `byte "a logging message"; log; int 1`, + errContains: "log not allowed in current mode", + runMode: runModeSignature, + }, + } + + for _, c := range failCases { + ops := testProg(t, c.source, AssemblerMaxVersion) + + err := CheckStateful(ops.Program, ep) + require.NoError(t, err, c) + + var pass bool + switch c.runMode { + case runModeApplication: + pass, err = EvalStateful(ops.Program, ep) + default: + pass, err = Eval(ops.Program, ep) + + } + require.Contains(t, err.Error(), c.errContains) + require.False(t, pass) + } +} diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index 6350856c7c..6bb2f63478 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -294,6 +294,9 @@ var OpSpecs = []OpSpec{ {0xad, "b^", opBytesBitXor, asmDefault, disDefault, twoBytes, oneBytes, 4, modeAny, costly(6)}, {0xae, "b~", opBytesBitNot, asmDefault, disDefault, oneBytes, oneBytes, 4, modeAny, costly(4)}, {0xaf, "bzero", opBytesZero, asmDefault, disDefault, oneInt, oneBytes, 4, modeAny, opDefault}, + + // ABI support opcodes. + {0xb0, "log", opLog, asmDefault, disDefault, oneBytes, nil, 5, runModeApplication, opDefault}, } type sortByOpcode []OpSpec diff --git a/ledger/appcow.go b/ledger/appcow.go index c1210cfa61..5cf3b568c7 100644 --- a/ledger/appcow.go +++ b/ledger/appcow.go @@ -456,6 +456,12 @@ func (cb *roundCowState) DelKey(addr basics.Address, aidx basics.AppIndex, globa return nil // note: deletion cannot cause us to violate maxCount } +// AppendLog adds message in logs. idx is expected to be an index in txn.ForeignApps +func (cb *roundCowState) AppendLog(idx uint64, value string) error { + cb.logs = append(cb.logs, basics.LogItem{ID: idx, Message: value}) + return nil +} + // MakeDebugBalances creates a ledger suitable for dryrun and debugger func MakeDebugBalances(l ledgerForCowBase, round basics.Round, proto protocol.ConsensusVersion, prevTimestamp int64) apply.Balances { base := &roundCowBase{ @@ -502,8 +508,9 @@ func (cb *roundCowState) StatefulEval(params logic.EvalParams, aidx basics.AppIn return pass, evalDelta, nil } -// BuildEvalDelta converts internal sdeltas into basics.EvalDelta +// BuildEvalDelta converts internal sdeltas and logs into basics.EvalDelta func (cb *roundCowState) BuildEvalDelta(aidx basics.AppIndex, txn *transactions.Transaction) (evalDelta basics.EvalDelta, err error) { + // sdeltas foundGlobal := false for addr, smod := range cb.sdeltas { for aapp, sdelta := range smod { @@ -550,6 +557,10 @@ func (cb *roundCowState) BuildEvalDelta(aidx basics.AppIndex, txn *transactions. } } } + + // logs + evalDelta.Logs = cb.logs + return } diff --git a/ledger/appcow_test.go b/ledger/appcow_test.go index 7eb467dcc4..92c226d5a4 100644 --- a/ledger/appcow_test.go +++ b/ledger/appcow_test.go @@ -599,6 +599,36 @@ func TestCowBuildDelta(t *testing.T) { }, ed, ) + + // check logDelta is added + cow.logs = []basics.LogItem{{ID: 0, Message: "hello,world"}} + cow.sdeltas[sender][storagePtr{aidx, false}] = &storageDelta{ + action: remainAllocAction, + kvCow: stateDelta{ + "key1": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 2}, + oldExists: true, + newExists: true, + }, + }, + accountIdx: 1, + } + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Equal( + basics.EvalDelta{ + GlobalDelta: basics.StateDelta(nil), + LocalDeltas: map[uint64]basics.StateDelta{ + 0: { + "key1": basics.ValueDelta{Action: basics.SetUintAction, Uint: 2}, + }, + }, + Logs: []basics.LogItem{{ID: 0, Message: "hello,world"}}, + }, + ed, + ) + } func TestCowDeltaSerialize(t *testing.T) { @@ -1328,3 +1358,19 @@ func TestCowDelKey(t *testing.T) { a.Panics(func() { c.DelKey(getRandomAddress(a), aidx, false, key, 0) }) a.Panics(func() { c.DelKey(addr, aidx+1, false, key, 0) }) } +func TestCowAppendLog(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + addr := getRandomAddress(a) + aidx := basics.AppIndex(0) + c := getCow([]modsData{ + {addr, basics.CreatableIndex(aidx), basics.AppCreatable}, + }) + + c.logs = []basics.LogItem{} + err := c.AppendLog(uint64(aidx), "val") + a.NoError(err) + a.Equal(len(c.logs), 1) +} diff --git a/ledger/applications.go b/ledger/applications.go index e03604a593..241b9c87bc 100644 --- a/ledger/applications.go +++ b/ledger/applications.go @@ -40,6 +40,8 @@ type cowForLogicLedger interface { SetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, value basics.TealValue, accountIdx uint64) error DelKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) error + AppendLog(idx uint64, value string) error + round() basics.Round prevTimestamp() int64 allocated(addr basics.Address, aidx basics.AppIndex, global bool) (bool, error) @@ -235,3 +237,14 @@ func (al *logicLedger) DelGlobal(key string) error { func (al *logicLedger) GetDelta(txn *transactions.Transaction) (evalDelta basics.EvalDelta, err error) { return al.cow.BuildEvalDelta(al.aidx, txn) } + +func (al *logicLedger) AppendLog(txn *transactions.Transaction, value string) error { + idx, err := txn.IndexByAppID(txn.ApplicationID) + if idx != 0 { + return fmt.Errorf("index offset is not 0. logging is allowed for current app only") + } + if err != nil { + return err + } + return al.cow.AppendLog(idx, value) +} diff --git a/ledger/applications_test.go b/ledger/applications_test.go index 4837b2a7d5..f64eb58810 100644 --- a/ledger/applications_test.go +++ b/ledger/applications_test.go @@ -61,6 +61,7 @@ type mockCowForLogicLedger struct { brs map[basics.Address]basics.AccountData stores map[storeLocator]basics.TealKeyValue tcs map[int]basics.CreatableIndex + logs []basics.LogItem } func (c *mockCowForLogicLedger) Get(addr basics.Address, withPendingRewards bool) (basics.AccountData, error) { @@ -126,6 +127,11 @@ func (c *mockCowForLogicLedger) allocated(addr basics.Address, aidx basics.AppIn return found, nil } +func (c *mockCowForLogicLedger) AppendLog(aidx uint64, value string) error { + c.logs = append(c.logs, basics.LogItem{ID: aidx, Message: value}) + return nil +} + func newCowMock(creatables []modsData) *mockCowForLogicLedger { var m mockCowForLogicLedger m.cr = make(map[creatableLocator]basics.Address, len(creatables)) @@ -1364,3 +1370,33 @@ func testAppAccountDeltaIndicesCompatibility(t *testing.T, source string, accoun a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[accountIdx], "lk1") a.Equal(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[accountIdx]["lk1"].Bytes, "local1") } + +func TestLogicLedgerAppendLog(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + addr := getRandomAddress(a) + aidx := basics.AppIndex(1) + c := newCowMock([]modsData{ + {addr, basics.CreatableIndex(1), basics.AppCreatable}, + }) + l, err := newLogicLedger(c, aidx) + a.NoError(err) + a.NotNil(l) + + appCallFields := transactions.ApplicationCallTxnFields{ + OnCompletion: transactions.NoOpOC, + ApplicationID: 0, + Accounts: []basics.Address{}, + } + appCall := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: appCallFields, + } + + err = l.AppendLog(&appCall, "a") + a.NoError(err) + a.Equal(len(c.logs), 1) + a.Equal(c.logs[0].Message, "a") +} diff --git a/ledger/apply/application_test.go b/ledger/apply/application_test.go index 90bd12320d..6938621cc0 100644 --- a/ledger/apply/application_test.go +++ b/ledger/apply/application_test.go @@ -839,6 +839,15 @@ func TestAppCallClearState(t *testing.T) { a.Equal(0, len(br.AppLocalStates)) a.Equal(basics.StateSchema{}, br.TotalAppSchema) a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) + + b.ResetWrites() + b.pass = true + b.err = nil + logs := []basics.LogItem{{ID: 0, Message: "a"}} + b.delta = basics.EvalDelta{Logs: []basics.LogItem{{ID: 0, Message: "a"}}} + err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) + a.NoError(err) + a.Equal(basics.EvalDelta{Logs: logs}, ad.EvalDelta) } func TestAppCallApplyCloseOut(t *testing.T) { @@ -921,6 +930,11 @@ func TestAppCallApplyCloseOut(t *testing.T) { a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) a.Equal(basics.StateSchema{NumUint: 0}, br.TotalAppSchema) + logs := []basics.LogItem{{ID: 0, Message: "a"}} + b.delta = basics.EvalDelta{Logs: []basics.LogItem{{ID: 0, Message: "a"}}} + err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) + a.NoError(err) + a.Equal(basics.EvalDelta{Logs: logs}, ad.EvalDelta) } func TestAppCallApplyUpdate(t *testing.T) { @@ -1015,6 +1029,12 @@ func TestAppCallApplyUpdate(t *testing.T) { b.balances[creator] = cp b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} + logs := []basics.LogItem{{ID: 0, Message: "a"}} + b.delta = basics.EvalDelta{Logs: []basics.LogItem{{ID: 0, Message: "a"}}} + err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) + a.NoError(err) + a.Equal(basics.EvalDelta{Logs: logs}, ad.EvalDelta) + // check extraProgramPages is used appr := make([]byte, 2*proto.MaxAppProgramLen+1) appr[0] = 4 // version 4 @@ -1065,6 +1085,7 @@ func TestAppCallApplyUpdate(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.Error(err) a.Contains(err.Error(), "updateApplication app programs too long") + } func TestAppCallApplyDelete(t *testing.T) { @@ -1176,6 +1197,11 @@ func TestAppCallApplyDelete(t *testing.T) { } b.ResetWrites() } + logs := []basics.LogItem{{ID: 0, Message: "a"}} + b.delta = basics.EvalDelta{Logs: []basics.LogItem{{ID: 0, Message: "a"}}} + err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) + a.NoError(err) + a.Equal(basics.EvalDelta{Logs: logs}, ad.EvalDelta) } func TestAppCallApplyCreateClearState(t *testing.T) { @@ -1270,4 +1296,11 @@ func TestAppCallApplyCreateDelete(t *testing.T) { a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) br := b.balances[creator] a.Equal(basics.AppParams{}, br.AppParams[appIdx]) + + logs := []basics.LogItem{{ID: 0, Message: "a"}} + b.delta = basics.EvalDelta{Logs: []basics.LogItem{{ID: 0, Message: "a"}}} + err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) + a.NoError(err) + a.Equal(basics.EvalDelta{Logs: logs}, ad.EvalDelta) + } diff --git a/ledger/cow.go b/ledger/cow.go index e0889575e2..c5595169ba 100644 --- a/ledger/cow.go +++ b/ledger/cow.go @@ -63,6 +63,9 @@ type roundCowState struct { // must be incorporated into mods.accts before passing deltas forward sdeltas map[basics.Address]map[storagePtr]*storageDelta + // logs populated in AppCall transaction + logs []basics.LogItem + // either or not maintain compatibility with original app refactoring behavior // this is needed for generating old eval delta in new code compatibilityMode bool @@ -83,6 +86,7 @@ func makeRoundCowState(b roundCowParent, hdr bookkeeping.BlockHeader, prevTimest mods: ledgercore.MakeStateDelta(&hdr, prevTimestamp, hint, 0), sdeltas: make(map[basics.Address]map[storagePtr]*storageDelta), trackedCreatables: make(map[int]basics.CreatableIndex), + logs: make([]basics.LogItem, 0), } // compatibilityMode retains producing application' eval deltas under the following rule: diff --git a/test/e2e-go/features/transactions/application_test.go b/test/e2e-go/features/transactions/application_test.go new file mode 100644 index 0000000000..5ad255790f --- /dev/null +++ b/test/e2e-go/features/transactions/application_test.go @@ -0,0 +1,130 @@ +// Copyright (C) 2019-2021 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package transactions + +import ( + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/framework/fixtures" + "github.com/algorand/go-algorand/test/partitiontest" +) + +func checkEqual(expected []basics.LogItem, actual []basics.LogItem) bool { + if len(expected) != len(actual) { + return false + } + for i, e := range expected { + if !e.Equal(actual[i]) { + return false + } + } + return true +} + +func TestApplication(t *testing.T) { + partitiontest.PartitionTest(t) + + t.Parallel() + a := require.New(fixtures.SynchronizedTest(t)) + + var fixture fixtures.RestClientFixture + proto, ok := config.Consensus[protocol.ConsensusFuture] + a.True(ok) + proto.AgreementFilterTimeoutPeriod0 = 400 * time.Millisecond + proto.AgreementFilterTimeout = 400 * time.Millisecond + fixture.SetConsensus(config.ConsensusProtocols{protocol.ConsensusFuture: proto}) + + fixture.Setup(t, filepath.Join("nettemplates", "TwoNodes50EachFuture.json")) + defer fixture.Shutdown() + + client := fixture.LibGoalClient + accountList, err := fixture.GetWalletsSortedByBalance() + a.NoError(err) + + creator := accountList[0].Address + wh, err := client.GetUnencryptedWalletHandle() + a.NoError(err) + + fee := uint64(1000) + + counter := `#pragma version 5 +int 1 +loop: byte "a" +log +int 1 ++ +dup +int 30 +<= +bnz loop +byte "b" +log +byte "c" +log +` + + approvalOps, err := logic.AssembleString(counter) + a.NoError(err) + clearstateOps, err := logic.AssembleString("#pragma version 5\nint 1") + a.NoError(err) + schema := basics.StateSchema{ + NumUint: 1, + } + + // create the app + tx, err := client.MakeUnsignedAppCreateTx( + transactions.OptInOC, approvalOps.Program, clearstateOps.Program, schema, schema, nil, nil, nil, nil, 0) + a.NoError(err) + tx, err = client.FillUnsignedTxTemplate(creator, 0, 0, fee, tx) + a.NoError(err) + wh, err = client.GetUnencryptedWalletHandle() + a.NoError(err) + signedTxn, err := client.SignTransactionWithWallet(wh, nil, tx) + a.NoError(err) + round, err := client.CurrentRound() + a.NoError(err) + txid, err := client.BroadcastTransaction(signedTxn) + a.NoError(err) + confirmed := fixture.WaitForAllTxnsToConfirm(round+2, map[string]string{txid: signedTxn.Txn.Sender.String()}) + a.True(confirmed) + round, err = client.CurrentRound() + a.NoError(err) + + logs := make([]basics.LogItem, 32) + for i := range logs { + logs[i] = basics.LogItem{ID: 0, Message: "a"} + } + logs[30] = basics.LogItem{ID: 0, Message: "b"} + logs[31] = basics.LogItem{ID: 0, Message: "c"} + + b, err := client.BookkeepingBlock(round) + for _, ps := range b.Payset { + ed := ps.ApplyData.EvalDelta + ok = checkEqual(logs, ed.Logs) + a.True(ok) + } + +} diff --git a/test/e2e-go/restAPI/restClient_test.go b/test/e2e-go/restAPI/restClient_test.go index 3c3faa2d0d..69db074c82 100644 --- a/test/e2e-go/restAPI/restClient_test.go +++ b/test/e2e-go/restAPI/restClient_test.go @@ -18,6 +18,7 @@ package restapi import ( "context" + "encoding/base64" "errors" "flag" "math" @@ -29,10 +30,12 @@ import ( "time" "unicode" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" algodclient "github.com/algorand/go-algorand/daemon/algod/api/client" kmdclient "github.com/algorand/go-algorand/daemon/kmd/client" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -851,7 +854,6 @@ func TestClientTruncatesPendingTransactions(t *testing.T) { a.NoError(err) txIDsSeen[tx2.ID().String()] = true } - statusResponse, err := testClient.GetPendingTransactions(uint64(MaxTxns)) a.NoError(err) a.NotEmpty(statusResponse) @@ -905,3 +907,130 @@ func TestClientPrioritizesPendingTransactions(t *testing.T) { a.True(len(statusResponse.TruncatedTxns.Transactions) == MaxTxns) a.True(statusResponse.TruncatedTxns.Transactions[0].TxID == txHigh.ID().String()) } + +func TestClientCanGetPendingTransactionInfo(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(fixtures.SynchronizedTest(t)) + var localFixture fixtures.RestClientFixture + localFixture.Setup(t, filepath.Join("nettemplates", "TwoNodes50EachFuture.json")) + defer localFixture.Shutdown() + + testClient := localFixture.LibGoalClient + + testClient.WaitForRound(1) + + testClient.SetAPIVersionAffinity(algodclient.APIVersionV2, kmdclient.APIVersionV1) + + wh, err := testClient.GetUnencryptedWalletHandle() + a.NoError(err) + addresses, err := testClient.ListAddresses(wh) + a.NoError(err) + _, someAddress := getMaxBalAddr(t, testClient, addresses) + if someAddress == "" { + t.Error("no addr with funds") + } + a.NoError(err) + addr, err := basics.UnmarshalChecksumAddress(someAddress) + + params, err := testClient.SuggestedParams() + a.NoError(err) + + firstRound := basics.Round(params.LastRound + 1) + lastRound := basics.Round(params.LastRound + 1000) + var gh crypto.Digest + copy(gh[:], params.GenesisHash) + + prog := `#pragma version 5 +byte "A" +loop: +int 0 +dup2 +getbyte +int 1 ++ +dup +int 97 //ascii code of last char +<= +bz end +setbyte +dup +log +b loop +end: +int 1 +return +` + ops, err := logic.AssembleString(prog) + approv := ops.Program + ops, err = logic.AssembleString("#pragma version 5 \nint 1") + clst := ops.Program + + gl := basics.StateSchema{ + NumByteSlice: 1, + } + lc := basics.StateSchema{ + NumByteSlice: 1, + } + minTxnFee, _, err := localFixture.CurrentMinFeeAndBalance() + + tx, err := testClient.MakeUnsignedApplicationCallTx(0, nil, addresses, nil, nil, transactions.NoOpOC, approv, clst, gl, lc, 0) + tx.Sender = addr + tx.Fee = basics.MicroAlgos{Raw: minTxnFee} + tx.FirstValid = firstRound + tx.LastValid = lastRound + tx.GenesisHash = gh + + txid, err := testClient.SignAndBroadcastTransaction(wh, nil, tx) + a.NoError(err) + _, err = waitForTransaction(t, testClient, someAddress, txid, 60*time.Second) + a.NoError(err) + txn, err := testClient.PendingTransactionInformationV2(txid) + a.NoError(err) + a.NotNil(txn.Logs) + a.Equal(32, len(*txn.Logs)) + for i, l := range *txn.Logs { + a.Equal(*txn.ApplicationIndex, l.Id) + assert.Equal(t, base64.StdEncoding.EncodeToString([]byte(string(rune('B'+i)))), l.Value) + } + + //check non-create app call + expectedAppID := *txn.ApplicationIndex + wh, err = testClient.GetUnencryptedWalletHandle() + a.NoError(err) + addresses, err = testClient.ListAddresses(wh) + a.NoError(err) + _, someAddress = getMaxBalAddr(t, testClient, addresses) + if someAddress == "" { + t.Error("no addr with funds") + } + a.NoError(err) + addr, err = basics.UnmarshalChecksumAddress(someAddress) + + params, err = testClient.SuggestedParams() + a.NoError(err) + + firstRound = basics.Round(params.LastRound + 1) + lastRound = basics.Round(params.LastRound + 1000) + + tx, err = testClient.MakeUnsignedAppNoOpTx(*txn.ApplicationIndex, nil, addresses, nil, nil) + tx.Sender = addr + tx.Fee = basics.MicroAlgos{Raw: minTxnFee} + tx.FirstValid = firstRound + tx.LastValid = lastRound + tx.GenesisHash = gh + + txid, err = testClient.SignAndBroadcastTransaction(wh, nil, tx) + a.NoError(err) + _, err = waitForTransaction(t, testClient, someAddress, txid, 60*time.Second) + a.NoError(err) + txn, err = testClient.PendingTransactionInformationV2(txid) + a.NoError(err) + a.NotNil(txn.Logs) + a.Equal(32, len(*txn.Logs)) + for i, l := range *txn.Logs { + a.Equal(expectedAppID, l.Id) + assert.Equal(t, base64.StdEncoding.EncodeToString([]byte(string(rune('B'+i)))), l.Value) + } + +}