Node/Watcher/Algorand: Handle Algorand inner transactions (#3072)

This commit is contained in:
Ben Guidarelli 2023-06-14 12:19:48 -04:00 committed by GitHub
parent 0fbfa816ba
commit 7f6213019a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 998 additions and 29 deletions

View File

@ -0,0 +1,867 @@
{
"Sig": [
105,
216,
195,
48,
134,
19,
231,
177,
203,
123,
39,
68,
177,
108,
49,
146,
16,
83,
252,
3,
205,
105,
155,
140,
65,
146,
72,
245,
27,
81,
157,
154,
27,
215,
57,
158,
162,
93,
67,
62,
209,
22,
227,
86,
199,
48,
33,
103,
121,
84,
4,
163,
0,
85,
27,
107,
141,
183,
243,
43,
22,
28,
134,
12
],
"Txn": {
"Type": "appl",
"Sender": [
206,
168,
201,
203,
219,
55,
61,
172,
61,
71,
177,
40,
245,
137,
33,
193,
111,
150,
6,
171,
13,
68,
81,
115,
156,
174,
167,
216,
238,
132,
137,
228
],
"Fee": 1000,
"FirstValid": 30453933,
"LastValid": 30454933,
"Note": null,
"ApplicationID": 231231217,
"OnCompletion": 0,
"ApplicationArgs": [
"hd0aKg==",
"nhxWOOMdX90rqGU4T5RVXpX+UXgAAAAADLE83Q==",
"AAAAAAAHoSA="
],
"Accounts": [
[
210,
200,
255,
178,
7,
225,
188,
18,
254,
137,
181,
25,
145,
41,
27,
249,
185,
43,
215,
225,
96,
64,
230,
126,
144,
206,
212,
182,
57,
154,
179,
83
],
[
137,
240,
161,
164,
216,
254,
162,
141,
120,
49,
250,
230,
145,
1,
126,
6,
19,
84,
124,
105,
213,
48,
63,
5,
170,
88,
13,
35,
36,
240,
151,
156
]
],
"ForeignApps": [
86525641,
86525623
],
"ForeignAssets": [
212942045
],
"BoxReferences": [
{
"ForeignAppIdx": 0,
"Name": "nhxWOOMdX90rqGU4T5RVXpX+UXgAAAAADLE83Q=="
}
],
"LocalStateSchema": {
"NumUint": 0,
"NumByteSlice": 0
},
"GlobalStateSchema": {
"NumUint": 0,
"NumByteSlice": 0
}
},
"EvalDelta": {
"Logs": [
"\u0015\u001f|u\u0000\u0000\u0000\u0000\u0000\u0007\ufffd "
],
"InnerTxns": [
{
"Sig": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"Txn": {
"Type": "appl",
"Sender": [
240,
31,
115,
228,
39,
220,
94,
207,
14,
189,
34,
156,
94,
159,
156,
17,
115,
89,
75,
74,
99,
141,
197,
86,
192,
137,
166,
117,
120,
234,
76,
214
],
"Fee": 3000,
"FirstValid": 30453933,
"LastValid": 30454933,
"Note": null,
"Group": [
80,
9,
220,
16,
247,
42,
178,
223,
143,
233,
199,
8,
80,
214,
173,
253,
32,
191,
226,
254,
247,
229,
92,
207,
12,
244,
184,
99,
83,
92,
146,
8
],
"ApplicationID": 86525641,
"OnCompletion": 0,
"ApplicationArgs": [
"bm9w"
]
}
},
{
"Txn": {
"Type": "axfer",
"Sender": [
240,
31,
115,
228,
39,
220,
94,
207,
14,
189,
34,
156,
94,
159,
156,
17,
115,
89,
75,
74,
99,
141,
197,
86,
192,
137,
166,
117,
120,
234,
76,
214
],
"Fee": 3000,
"FirstValid": 30453933,
"LastValid": 30454933,
"Group": [
80,
9,
220,
16,
247,
42,
178,
223,
143,
233,
199,
8,
80,
214,
173,
253,
32,
191,
226,
254,
247,
229,
92,
207,
12,
244,
184,
99,
83,
92,
146,
8
],
"XferAsset": 212942045,
"AssetAmount": 500000,
"AssetReceiver": [
137,
240,
161,
164,
216,
254,
162,
141,
120,
49,
250,
230,
145,
1,
126,
6,
19,
84,
124,
105,
213,
48,
63,
5,
170,
88,
13,
35,
36,
240,
151,
156
]
},
"AuthAddr": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"ClosingAmount": 0,
"AssetClosingAmount": 0,
"SenderRewards": 0,
"ReceiverRewards": 0,
"CloseRewards": 0,
"EvalDelta": {
"GlobalDelta": null,
"LocalDeltas": null,
"Logs": null,
"InnerTxns": null
},
"ConfigAsset": 0,
"ApplicationID": 0
},
{
"Txn": {
"Type": "appl",
"Sender": [
240,
31,
115,
228,
39,
220,
94,
207,
14,
189,
34,
156,
94,
159,
156,
17,
115,
89,
75,
74,
99,
141,
197,
86,
192,
137,
166,
117,
120,
234,
76,
214
],
"Fee": 3000,
"FirstValid": 30453933,
"LastValid": 30454933,
"Note": null,
"GenesisID": "",
"Group": [
80,
9,
220,
16,
247,
42,
178,
223,
143,
233,
199,
8,
80,
214,
173,
253,
32,
191,
226,
254,
247,
229,
92,
207,
12,
244,
184,
99,
83,
92,
146,
8
],
"ApplicationID": 86525641,
"OnCompletion": 0,
"ApplicationArgs": [
"c2VuZFRyYW5zZmVy",
"AAAAAAyxPN0=",
"AAAAAAAHoSA=",
"AAAAAAAAAAAAAAAAnhxWOOMdX90rqGU4T5RVXpX+UXg=",
"AAAAAAAAAAU=",
"AAAAAAAAAAA="
],
"Accounts": [
[
210,
200,
255,
178,
7,
225,
188,
18,
254,
137,
181,
25,
145,
41,
27,
249,
185,
43,
215,
225,
96,
64,
230,
126,
144,
206,
212,
182,
57,
154,
179,
83
],
[
137,
240,
161,
164,
216,
254,
162,
141,
120,
49,
250,
230,
145,
1,
126,
6,
19,
84,
124,
105,
213,
48,
63,
5,
170,
88,
13,
35,
36,
240,
151,
156
],
[
137,
240,
161,
164,
216,
254,
162,
141,
120,
49,
250,
230,
145,
1,
126,
6,
19,
84,
124,
105,
213,
48,
63,
5,
170,
88,
13,
35,
36,
240,
151,
156
]
],
"ForeignApps": [
86525623
],
"ForeignAssets": [
212942045
],
"BoxReferences": null,
"LocalStateSchema": {
"NumUint": 0,
"NumByteSlice": 0
},
"GlobalStateSchema": {
"NumUint": 0,
"NumByteSlice": 0
}
},
"EvalDelta": {
"GlobalDelta": null,
"LocalDeltas": null,
"Logs": null,
"InnerTxns": [
{
"Txn": {
"Type": "appl",
"Sender": [
98,
65,
255,
220,
3,
43,
105,
59,
251,
133,
68,
133,
143,
4,
3,
222,
200,
111,
46,
23,
32,
175,
159,
52,
248,
214,
95,
229,
116,
182,
35,
140
],
"Fee": 0,
"FirstValid": 30453933,
"LastValid": 30454933,
"Note": "cHVibGlzaE1lc3NhZ2U=",
"AssetFrozen": false,
"ApplicationID": 86525623,
"OnCompletion": 0,
"ApplicationArgs": [
"cHVibGlzaE1lc3NhZ2U=",
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6EgAAAAAAAAAAAAAAAAnDySg9PkSFRpfNItP6okDPsDKIkABQAAAAAAAAAAAAAAAJ4cVjjjHV/dK6hlOE+UVV6V/lF4AAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"AAAAAAAAAAA="
],
"Accounts": [
[
210,
200,
255,
178,
7,
225,
188,
18,
254,
137,
181,
25,
145,
41,
27,
249,
185,
43,
215,
225,
96,
64,
230,
126,
144,
206,
212,
182,
57,
154,
179,
83
]
],
"ForeignApps": null,
"ForeignAssets": null,
"BoxReferences": null,
"LocalStateSchema": {
"NumUint": 0,
"NumByteSlice": 0
},
"GlobalStateSchema": {
"NumUint": 0,
"NumByteSlice": 0
},
"ApprovalProgram": null,
"ClearStateProgram": null,
"ExtraProgramPages": 0,
"StateProofType": 0,
"StateProof": null,
"Message": {
"BlockHeadersCommitment": null,
"VotersCommitment": null,
"LnProvenWeight": 0,
"FirstAttestedRound": 0,
"LastAttestedRound": 0
}
},
"EvalDelta": {
"GlobalDelta": null,
"LocalDeltas": {
"1": {
"\u0000": {
"Action": 1,
"Bytes": "\u0000\u0000\u0000\u0000\u0000\u0000\u0003\ufffd\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
"Uint": 0
}
}
},
"Logs": [
"AAAAAAAAA+E="
],
"InnerTxns": null
},
"ConfigAsset": 0,
"ApplicationID": 0
}
]
},
"ConfigAsset": 0,
"ApplicationID": 0
}
]
},
"HasGenesisID": true,
"HasGenesisHash": false
}

View File

@ -24,6 +24,9 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// Algorand allows max depth of 8 inner transactions
const MAX_DEPTH = 8
type ( type (
// Watcher is responsible for looking over Algorand blockchain and reporting new transactions to the appid // Watcher is responsible for looking over Algorand blockchain and reporting new transactions to the appid
Watcher struct { Watcher struct {
@ -39,6 +42,13 @@ type (
next_round uint64 next_round uint64
} }
algorandObservation struct {
emitterAddress vaa.Address
nonce uint32
sequence uint64
payload []byte
}
) )
var ( var (
@ -77,31 +87,60 @@ func NewWatcher(
} }
} }
// gatherObservations recurses through a given transactions inner-transactions
// to find any messages emitted from the core wormhole contract.
// Algorand allows up to 8 levels of inner transactions.
func gatherObservations(e *Watcher, t types.SignedTxnWithAD, depth int, logger *zap.Logger) (obs []algorandObservation) {
// SECURITY defense-in-depth: don't recurse > max depth allowed by Algorand
if depth >= MAX_DEPTH {
logger.Error("algod client", zap.Error(fmt.Errorf("exceeded max depth of %d", MAX_DEPTH)))
return
}
// recurse through nested inner transactions
for _, itxn := range t.EvalDelta.InnerTxns {
obs = append(obs, gatherObservations(e, itxn, depth+1, logger)...)
}
var at = t.Txn
var ed = t.EvalDelta
// check if the current transaction meets what we expect
// for an emitted message
if (len(at.ApplicationArgs) != 3) || (uint64(at.ApplicationID) != e.appid) || string(at.ApplicationArgs[0]) != "publishMessage" || len(ed.Logs) == 0 {
return
}
logger.Info("emitter: " + hex.EncodeToString(at.Sender[:]))
var a vaa.Address
copy(a[:], at.Sender[:]) // 32 bytes = 8edf5b0e108c3a1a0a4b704cc89591f2ad8d50df24e991567e640ed720a94be2
obs = append(obs, algorandObservation{
nonce: uint32(binary.BigEndian.Uint64(at.ApplicationArgs[2])),
sequence: binary.BigEndian.Uint64([]byte(ed.Logs[0])),
emitterAddress: a,
payload: at.ApplicationArgs[1],
})
return
}
// lookAtTxn takes an outer transaction from the block.payset and gathers
// observations from messages emitted in nested inner transactions
// then passes them on the relevant channels
func lookAtTxn(e *Watcher, t types.SignedTxnInBlock, b types.Block, logger *zap.Logger) { func lookAtTxn(e *Watcher, t types.SignedTxnInBlock, b types.Block, logger *zap.Logger) {
for q := 0; q < len(t.EvalDelta.InnerTxns); q++ {
var it = t.EvalDelta.InnerTxns[q]
var at = it.Txn
if (len(at.ApplicationArgs) != 3) || (uint64(at.ApplicationID) != e.appid) { observations := gatherObservations(e, t.SignedTxnWithAD, 0, logger)
continue
}
if string(at.ApplicationArgs[0]) != "publishMessage" {
continue
}
var ed = it.EvalDelta
if len(ed.Logs) == 0 {
continue
}
emitter := at.Sender
var a vaa.Address
copy(a[:], emitter[:]) // 32 bytes = 8edf5b0e108c3a1a0a4b704cc89591f2ad8d50df24e991567e640ed720a94be2
logger.Info("emitter: " + hex.EncodeToString(emitter[:]))
// We use the outermost transaction id in the observation message
// so we can apply the same logic to gather any messages emitted
// by inner transactions
var txHash eth_common.Hash
if len(observations) > 0 {
// Repopulate the genesis id/hash for the transaction
// since in the block encoding, it's omitted to save space
t.Txn.GenesisID = b.GenesisID t.Txn.GenesisID = b.GenesisID
t.Txn.GenesisHash = b.GenesisHash t.Txn.GenesisHash = b.GenesisHash
Id := crypto.GetTxID(t.Txn) Id := crypto.GetTxID(t.Txn)
@ -109,21 +148,22 @@ func lookAtTxn(e *Watcher, t types.SignedTxnInBlock, b types.Block, logger *zap.
id, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(Id) id, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(Id)
if err != nil { if err != nil {
logger.Error("Base32 DecodeString", zap.Error(err)) logger.Error("Base32 DecodeString", zap.Error(err))
continue return
} }
logger.Info("id: " + hex.EncodeToString(id) + " " + Id) logger.Info("id: " + hex.EncodeToString(id) + " " + Id)
var txHash = eth_common.BytesToHash(id) // 32 bytes = d3b136a6a182a40554b2fafbc8d12a7a22737c10c81e33b33d1dcb74c532708b txHash = eth_common.BytesToHash(id) // 32 bytes = d3b136a6a182a40554b2fafbc8d12a7a22737c10c81e33b33d1dcb74c532708b
}
for _, obs := range observations {
observation := &common.MessagePublication{ observation := &common.MessagePublication{
TxHash: txHash, TxHash: txHash,
Timestamp: time.Unix(b.TimeStamp, 0), Timestamp: time.Unix(b.TimeStamp, 0),
Nonce: uint32(binary.BigEndian.Uint64(at.ApplicationArgs[2])), Nonce: obs.nonce,
Sequence: binary.BigEndian.Uint64([]byte(ed.Logs[0])), Sequence: obs.sequence,
EmitterChain: vaa.ChainIDAlgorand, EmitterChain: vaa.ChainIDAlgorand,
EmitterAddress: a, EmitterAddress: obs.emitterAddress,
Payload: at.ApplicationArgs[1], Payload: obs.payload,
ConsistencyLevel: 0, ConsistencyLevel: 0,
} }

View File

@ -0,0 +1,62 @@
package algorand
import (
"encoding/base64"
"encoding/json"
"os"
"testing"
"github.com/algorand/go-algorand-sdk/types"
"github.com/certusone/wormhole/node/pkg/common"
gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
"go.uber.org/zap"
)
const APP_ID = 86525623
// Tests for nested inner transactions calling the core bridge
func TestLookAtTxnInnerTxn(t *testing.T) {
// Setup a watcher
msgC := make(chan *common.MessagePublication)
obsvReqC := make(chan *gossipv1.ObservationRequest, 50)
w := NewWatcher("", "", "", "", APP_ID, msgC, obsvReqC)
var expectedSequence uint64 = 993
// read in test block for inner transactions
b, err := os.ReadFile("test_nested_inner.block.json")
if err != nil {
t.Fatalf("failed to read block file: %s", err)
}
txn := types.SignedTxnInBlock{}
err = json.Unmarshal(b, &txn)
if err != nil {
t.Fatalf("failed to unmarshal block: %s", err)
}
// Because we are using a json blob and the type of logs array is []string
// and because go json package will refuse to properly encode/decode
// invalid utf8 characters, the json blob has the relevant log encoded as base64
// and we base64 decode it and convert it to a string _manually_ so we can
// make sure we got the right sequence number
b64Data := txn.EvalDelta.InnerTxns[2].EvalDelta.InnerTxns[0].EvalDelta.Logs[0]
bb, err := base64.StdEncoding.DecodeString(b64Data)
if err != nil {
t.Fatalf("Cant decode: %s", err)
}
txn.EvalDelta.InnerTxns[2].EvalDelta.InnerTxns[0].EvalDelta.Logs[0] = string(bb)
// for each tx in the block, check to see if its a valid
// wh emitted message
logger, _ := zap.NewProduction()
observations := gatherObservations(w, txn.SignedTxnWithAD, 0, logger)
if len(observations) != 1 {
t.Fatalf("expected 1 observation, got %d", len(observations))
}
if observations[0].sequence != expectedSequence {
t.Fatalf("expected sequence observed to be %d, got %d", expectedSequence, observations[0].sequence)
}
}