diff --git a/cmd/monitoring/main.go b/cmd/monitoring/main.go index 16ba24456..00512d2c2 100644 --- a/cmd/monitoring/main.go +++ b/cmd/monitoring/main.go @@ -18,7 +18,7 @@ func main() { } defer func() { if serr := log.Sync(); serr != nil { - fmt.Println(fmt.Sprintf("Error while closing Logger: %v", serr)) + fmt.Printf("Error while closing Logger: %v\n", serr) } }() diff --git a/pkg/monitoring/chain_reader.go b/pkg/monitoring/chain_reader.go index 6fdbf4df8..142ded9b3 100644 --- a/pkg/monitoring/chain_reader.go +++ b/pkg/monitoring/chain_reader.go @@ -15,6 +15,7 @@ type ChainReader interface { GetTokenAccountBalance(ctx context.Context, account solana.PublicKey, commitment rpc.CommitmentType) (out *rpc.GetTokenAccountBalanceResult, err error) GetBalance(ctx context.Context, account solana.PublicKey, commitment rpc.CommitmentType) (out *rpc.GetBalanceResult, err error) GetSignaturesForAddressWithOpts(ctx context.Context, account solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) (out []*rpc.TransactionSignature, err error) + GetTransaction(ctx context.Context, txSig solana.Signature, opts *rpc.GetTransactionOpts) (out *rpc.GetTransactionResult, err error) } func NewChainReader(client *rpc.Client) ChainReader { @@ -44,3 +45,7 @@ func (c *chainReader) GetBalance(ctx context.Context, account solana.PublicKey, func (c *chainReader) GetSignaturesForAddressWithOpts(ctx context.Context, account solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) (out []*rpc.TransactionSignature, err error) { return c.client.GetSignaturesForAddressWithOpts(ctx, account, opts) } + +func (c *chainReader) GetTransaction(ctx context.Context, txSig solana.Signature, opts *rpc.GetTransactionOpts) (out *rpc.GetTransactionResult, err error) { + return c.client.GetTransaction(ctx, txSig, opts) +} diff --git a/pkg/monitoring/event/decode.go b/pkg/monitoring/event/decode.go new file mode 100644 index 000000000..fdf8572c6 --- /dev/null +++ b/pkg/monitoring/event/decode.go @@ -0,0 +1,100 @@ +package event + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "fmt" + "regexp" +) + +var programInvocation = regexp.MustCompile(`^Program\s([a-zA-Z0-9]+)?\sinvoke\s\[\d\]$`) +var programFinished = regexp.MustCompile(`^Program\s([a-zA-Z0-9]+)?\s(?:success|error)$`) +var programLogEvent = regexp.MustCompile(`^Program\s(?:log|data):\s([+/0-9A-Za-z]+={0,2})?$`) + +func ExtractEvents(logs []string, programIDBase58 string) []string { + invocationStack := []string{} + output := []string{} + for _, log := range logs { + if matches := programInvocation.FindStringSubmatch(log); matches != nil { + invokedProgramID := matches[1] + invocationStack = append(invocationStack, invokedProgramID) + continue + } + if matches := programLogEvent.FindStringSubmatch(log); matches != nil { + currentProgramID := invocationStack[len(invocationStack)-1] + if programIDBase58 == currentProgramID { + output = append(output, matches[1]) + } + continue + } + if matches := programFinished.FindStringSubmatch(log); matches != nil { + if len(invocationStack) == 0 { + break // incorrect execution trace. + } + finishedProgramID := matches[1] + if invocationStack[len(invocationStack)-1] == finishedProgramID { + invocationStack = invocationStack[:len(invocationStack)-1] + } + } + } + return output +} + +// Decode extracts an event from the the encoded event given as a string. +func Decode(base64Encoded string) (interface{}, error) { + buf, err := base64.StdEncoding.DecodeString(base64Encoded) + if err != nil { + return nil, fmt.Errorf("failed to decode event '%s' from base64: %w", base64Encoded, err) + } + if len(buf) < discriminatorLength { + return nil, fmt.Errorf("expected event data to have at least %d bytes, but had %d", discriminatorLength, len(buf)) + } + discriminator, encoded := buf[:discriminatorLength], buf[discriminatorLength:] + switch true { + case bytes.Equal(discriminator, SetConfigDiscriminator): + var event SetConfig + if err = event.UnmarshalBinary(encoded); err != nil { + return nil, fmt.Errorf("failed to decode event '%v' of type '%T': %w", encoded, event, err) + } + return event, nil + case bytes.Equal(discriminator, SetBillingDiscriminator): + var event SetBilling + if err = event.UnmarshalBinary(encoded); err != nil { + return nil, fmt.Errorf("failed to decode event '%v' of type '%T': %w", encoded, event, err) + } + return event, nil + case bytes.Equal(discriminator, RoundRequestedDiscriminator): + var event RoundRequested + if err = event.UnmarshalBinary(encoded); err != nil { + return nil, fmt.Errorf("failed to decode event '%v' of type '%T': %w", encoded, event, err) + } + return event, nil + case bytes.Equal(discriminator, NewTransmissionDiscriminator): + var event NewTransmission + if err = event.UnmarshalBinary(encoded); err != nil { + return nil, fmt.Errorf("failed to decode event '%v' of type '%T': %w", encoded, event, err) + } + return event, nil + } + return nil, fmt.Errorf("Unrecognised event discriminator %x", discriminator) +} + +func DecodeMultiple(base64EncodedEvents []string) ([]interface{}, error) { + events := []interface{}{} + for _, encoded := range base64EncodedEvents { + event, err := Decode(encoded) + if err != nil { + return nil, err + } + events = append(events, event) + } + return events, nil +} + +const discriminatorLength = 8 + +func getDiscriminator(prefix string) []byte { + hash := sha256.Sum256([]byte(prefix)) + return hash[:discriminatorLength] +} diff --git a/pkg/monitoring/event/decode_test.go b/pkg/monitoring/event/decode_test.go new file mode 100644 index 000000000..849e19773 --- /dev/null +++ b/pkg/monitoring/event/decode_test.go @@ -0,0 +1,202 @@ +package event + +import ( + "encoding/binary" + "fmt" + "testing" + + bin "github.com/gagliardetto/binary" + "github.com/stretchr/testify/require" +) + +func TestExtractEvents(t *testing.T) { + programIDBase58 := "STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3" + groupsOfLogs := [][]string{ + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program log: gjbLTR5rT6iaSQcAAAMQumV5CqMwMWjU5bBudJS4G7Kr1YGm1javi5Tpf4Y3dOLMJAAAAAAAAAAAAAAAAigtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn invoke [2]", + "Program log: Instruction: Submit", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn consumed 3587 of 22659 compute units", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 181502 of 200000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program log: gjbLTR5rT6gSuQgAAAMuN5qPxmWZqcAitDRnFkdaJhqJ0WBRnjrLH9CzWkg3dOLMJAAAAAAAAAAAAAAACQ8tRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn invoke [2]", + "Program log: Instruction: Submit", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn consumed 3587 of 123958 compute units", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 80203 of 200000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program log: gjbLTR5rT6inQwcAAANy+x/LIETrs7naC0mc49puOD3+fSA+Mmunk2j5gKg3dOLMJAAAAAAAAAAAAAAAAA8tRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn invoke [2]", + "Program log: Instruction: Submit", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn consumed 3587 of 22709 compute units", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 181452 of 200000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program log: gjbLTR5rT6jhSwcAAAPLxuP0SjlzlEc3F2dlPyLOzIAeQnF05dG067WUiq43dOLMJAAAAAAAAAAAAAAAChAtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn invoke [2]", + "Program log: Instruction: Submit", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn consumed 3587 of 22243 compute units", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 181918 of 200000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program log: gjbLTR5rT6iSSQcAAAMQumV5CqMwMWjU5bBudJS4G7Kr1YGm1javi5Tpf4Y3dOLMJAAAAAAAAAAAAAAABhAtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn invoke [2]", + "Program log: Instruction: Submit", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn consumed 3587 of 22513 compute units", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 181648 of 200000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program log: gjbLTR5rT6gFSwcAAAOO/bNYwbBGoNcZhvTwFVHkSRI9vN9nDBQaU9Ocfy03dOLMJAAAAAAAAAAAAAAAAREtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn invoke [2]", + "Program log: Instruction: Submit", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn consumed 3587 of 22743 compute units", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 181418 of 200000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program log: gjbLTR5rT6iwQwcAAAM6wHcIzwrEysN7tds4vrXRJIBZlnB3bbc/91U47g03dOLMJAAAAAAAAAAAAAAACBQtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn invoke [2]", + "Program log: Instruction: Submit", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn consumed 3587 of 22217 compute units", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 181944 of 200000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program log: gjbLTR5rT6iUSQcAAAMQumV5CqMwMWjU5bBudJS4G7Kr1YGm1javi5Tpf4Y3dOLMJAAAAAAAAAAAAAAAAhYtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn invoke [2]", + "Program log: Instruction: Submit", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn consumed 3587 of 22616 compute units", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 181545 of 200000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program log: gjbLTR5rT6iqQwcAAANy+x/LIETrs7naC0mc49puOD3+fSA+Mmunk2j5gKg3dOLMJAAAAAAAAAAAAAAAChgtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn invoke [2]", + "Program log: Instruction: Submit", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn consumed 3587 of 22329 compute units", + "Program STGxAk2tuSMv7iwt2vRRuijRp1ageiRcwrjhdPBsAXn success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 181832 of 200000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program data: gjbLTR5rT6j9IRcAAAM0cArd3JbxinfsblA0z3qwRRlKQpralO0xSE8aPrh4zfUFAAAAAAAAAAAAAAAADW77smIPAwkHCgQPBgAMCw4BBQ0CAAAAAMxmujUBAAAAfWUAAAAAAAA=", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny invoke [2]", + "Program log: Instruction: Submit", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny consumed 3461 of 1212284 compute units", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny success", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 191737 of 1400000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { // The execution trace is not correct, the contract calls don't return with either success or failure. + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny invoke [2]", + "Program CcPVS9bqyXbD9cLnTbhhHazLsrua8QMFUHTutPtjyDzq invoke [3]", + "Program 7CLo1BY41BHAVnEs57kzYMnWXyBJrVEBPpZyQyPo2p1G invoke [4]", + "Program EH32v4UHcwH6S7gLTRvEBEyCTJrVbhRiJE7QEGoqd4NU invoke [5]", + }, + { // The execution trace is not correct, there are no contract invocations, only returns. + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny error", + "Program CcPVS9bqyXbD9cLnTbhhHazLsrua8QMFUHTutPtjyDzq success", + "Program 7CLo1BY41BHAVnEs57kzYMnWXyBJrVEBPpZyQyPo2p1G error", + "Program EH32v4UHcwH6S7gLTRvEBEyCTJrVbhRiJE7QEGoqd4NU success", + }, + { // The data for aggregator program appears after the inner contract call. This should still be recorded. + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny invoke [2]", + "Program log: Instruction: Submit", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny consumed 3461 of 1212284 compute units", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny success", + "Program data: gjbLTR5rT6j9IRcAAAM0cArd3JbxinfsblA0z3qwRRlKQpralO0xSE8aPrh4zfUFAAAAAAAAAAAAAAAADW77smIPAwkHCgQPBgAMCw4BBQ0CAAAAAMxmujUBAAAAfWUAAAAAAAA=", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 191737 of 1400000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + }, + { // Unexpected termination of a program that wasn't invoked, or doesn't appear in the trace log. + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [1]", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny invoke [2]", + "Program log: Instruction: Submit", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny consumed 3461 of 1212284 compute units", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny success", + "Program data: gjbLTR5rT6j9IRcAAAM0cArd3JbxinfsblA0z3qwRRlKQpralO0xSE8aPrh4zfUFAAAAAAAAAAAAAAAADW77smIPAwkHCgQPBgAMCw4BBQ0CAAAAAMxmujUBAAAAfWUAAAAAAAA=", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 191737 of 1400000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + "Program EH32v4UHcwH6S7gLTRvEBEyCTJrVbhRiJE7QEGoqd4NU success", + }, + { // Multiple programs in the call stack emit log data. + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny invoke [1]", + "Program data: gjbLTR5rT6j9IRcAAAM0cArd3JbxinfsblA0z3qwRRlKQpralO0xSE8aPrh4zfUFAAAAAAAAAAAAAAAADW77smIPAwkHCgQPBgAMCw4BBQ0CAAAAAMxmujUBAAAAfWUAAAAAAAA=", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 invoke [2]", + "Program data: jbLbTR5rT6j9IRcAAAM0cArd3JbxinfsblA0z3qwRRlKQpralO0xSE8aPrh4zfUFBBBBBBBBBBBBBBBBDW77smIPAwkHCgQPBgAMCw4BBQ0CAAAAAMxmujUBAAAAfWUAAAAAAAA=", + "Program log: Instruction: Submit", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 consumed 191737 of 1400000 compute units", + "Program STGhiM1ZaLjDLZDGcVFp3ppdetggLAs6MXezw5DXXH3 success", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny consumed 3461 of 1212284 compute units", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny success", + }, + } + expectedEvents := [][]string{ + {"gjbLTR5rT6iaSQcAAAMQumV5CqMwMWjU5bBudJS4G7Kr1YGm1javi5Tpf4Y3dOLMJAAAAAAAAAAAAAAAAigtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA="}, + {"gjbLTR5rT6gSuQgAAAMuN5qPxmWZqcAitDRnFkdaJhqJ0WBRnjrLH9CzWkg3dOLMJAAAAAAAAAAAAAAACQ8tRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA="}, + {"gjbLTR5rT6inQwcAAANy+x/LIETrs7naC0mc49puOD3+fSA+Mmunk2j5gKg3dOLMJAAAAAAAAAAAAAAAAA8tRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA="}, + {"gjbLTR5rT6jhSwcAAAPLxuP0SjlzlEc3F2dlPyLOzIAeQnF05dG067WUiq43dOLMJAAAAAAAAAAAAAAAChAtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA="}, + {"gjbLTR5rT6iSSQcAAAMQumV5CqMwMWjU5bBudJS4G7Kr1YGm1javi5Tpf4Y3dOLMJAAAAAAAAAAAAAAABhAtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA="}, + {"gjbLTR5rT6gFSwcAAAOO/bNYwbBGoNcZhvTwFVHkSRI9vN9nDBQaU9Ocfy03dOLMJAAAAAAAAAAAAAAAAREtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA="}, + {"gjbLTR5rT6iwQwcAAAM6wHcIzwrEysN7tds4vrXRJIBZlnB3bbc/91U47g03dOLMJAAAAAAAAAAAAAAACBQtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA="}, + {"gjbLTR5rT6iUSQcAAAMQumV5CqMwMWjU5bBudJS4G7Kr1YGm1javi5Tpf4Y3dOLMJAAAAAAAAAAAAAAAAhYtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA="}, + {"gjbLTR5rT6iqQwcAAANy+x/LIETrs7naC0mc49puOD3+fSA+Mmunk2j5gKg3dOLMJAAAAAAAAAAAAAAAChgtRmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA="}, + {"gjbLTR5rT6j9IRcAAAM0cArd3JbxinfsblA0z3qwRRlKQpralO0xSE8aPrh4zfUFAAAAAAAAAAAAAAAADW77smIPAwkHCgQPBgAMCw4BBQ0CAAAAAMxmujUBAAAAfWUAAAAAAAA="}, + {}, // no event found + {}, // no event found + {"gjbLTR5rT6j9IRcAAAM0cArd3JbxinfsblA0z3qwRRlKQpralO0xSE8aPrh4zfUFAAAAAAAAAAAAAAAADW77smIPAwkHCgQPBgAMCw4BBQ0CAAAAAMxmujUBAAAAfWUAAAAAAAA="}, + {"gjbLTR5rT6j9IRcAAAM0cArd3JbxinfsblA0z3qwRRlKQpralO0xSE8aPrh4zfUFAAAAAAAAAAAAAAAADW77smIPAwkHCgQPBgAMCw4BBQ0CAAAAAMxmujUBAAAAfWUAAAAAAAA="}, + {"jbLbTR5rT6j9IRcAAAM0cArd3JbxinfsblA0z3qwRRlKQpralO0xSE8aPrh4zfUFBBBBBBBBBBBBBBBBDW77smIPAwkHCgQPBgAMCw4BBQ0CAAAAAMxmujUBAAAAfWUAAAAAAAA="}, + } + require.Equal(t, len(expectedEvents), len(groupsOfLogs)) + for i, logs := range groupsOfLogs { + actualEvents := ExtractEvents(logs, programIDBase58) + require.Equal(t, expectedEvents[i], actualEvents, fmt.Sprintf("failed test case #%d", i)) + } +} + +func TestDecode(t *testing.T) { + encoded := "gjbLTR5rT6gW2QgAAAPLxuP0SjlzlEc3F2dlPyLOzIAeQnF05dG067WUiq7xYyfUMAAAAAAAAAAAAAAADZTbSmIQCAEOCQ8EBgcFAwoLDA0CAAAAAADKmjsAAAAAiBMAAAAAAAA=" + expected := NewTransmission{ + RoundID: 0x8d916, + ConfigDigest: [32]uint8{0x0, 0x3, 0xcb, 0xc6, 0xe3, 0xf4, 0x4a, 0x39, 0x73, 0x94, 0x47, 0x37, 0x17, 0x67, 0x65, 0x3f, 0x22, 0xce, 0xcc, 0x80, 0x1e, 0x42, 0x71, 0x74, 0xe5, 0xd1, 0xb4, 0xeb, 0xb5, 0x94, 0x8a, 0xae}, + Answer: bin.Int128{Lo: 0x30d42763f1, Hi: 0x0, Endianness: binary.ByteOrder(nil)}, + Transmitter: 0xd, + ObservationsTimestamp: 0x624adb94, + ObserverCount: 0x10, + Observers: [19]uint8{0x8, 0x1, 0xe, 0x9, 0xf, 0x4, 0x6, 0x7, 0x5, 0x3, 0xa, 0xb, 0xc, 0xd, 0x2, 0x0, 0x0, 0x0, 0x0}, + JuelsPerLamport: 0x3b9aca00, + ReimbursementGJuels: 0x1388, + } + decoded, err := Decode(encoded) + require.NoError(t, err) + require.Equal(t, expected, decoded) +} diff --git a/pkg/monitoring/event/types.go b/pkg/monitoring/event/types.go new file mode 100644 index 000000000..4e3a36b1d --- /dev/null +++ b/pkg/monitoring/event/types.go @@ -0,0 +1,64 @@ +package event + +import ( + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" +) + +var ( + SetConfigDiscriminator = getDiscriminator("event:SetConfig") + SetBillingDiscriminator = getDiscriminator("event:SetBilling") + RoundRequestedDiscriminator = getDiscriminator("event:RoundRequested") + NewTransmissionDiscriminator = getDiscriminator("event:NewTransmission") +) + +type SetConfig struct { + ConfigDigest [32]uint8 `json:"config_digest,omitempty"` + F uint8 `json:"f,omitempty"` + Signers [][20]uint8 `json:"signers,omitempty"` +} + +// UnmarshalBinary makes SetConfig implement encoding.BinaryUnmarshaler +// We manually decode the data because gagliardetto/binary deoes not support slices needed for Signers. +func (s *SetConfig) UnmarshalBinary(data []byte) error { + return bin.NewBinDecoder(data).Decode(s) +} + +type SetBilling struct { + ObservationPaymentGJuels uint32 `json:"observation_payment_gjuels,omitempty"` + TransmissionPaymentGJuels uint32 `json:"transmission_payment_gjuels,omitempty"` +} + +// UnmarshalBinary makes SetBilling implement encoding.BinaryUnmarshaler +func (s *SetBilling) UnmarshalBinary(data []byte) error { + return bin.NewBinDecoder(data).Decode(s) +} + +type RoundRequested struct { + ConfigDigest [32]uint8 `json:"config_digest,omitempty"` + Requester solana.PublicKey `json:"requester,omitempty"` + Epoch uint32 `json:"epoch,omitempty"` + Round uint8 `json:"round,omitempty"` +} + +// UnmarshalBinary makes RoundRequested implement encoding.BinaryUnmarshaler +func (r *RoundRequested) UnmarshalBinary(data []byte) error { + return bin.NewBinDecoder(data).Decode(r) +} + +type NewTransmission struct { + RoundID uint32 `json:"round_id,omitempty"` + ConfigDigest [32]uint8 `json:"config_digest,omitempty"` + Answer bin.Int128 `json:"answer,omitempty"` + Transmitter uint8 `json:"transmitter,omitempty"` + ObservationsTimestamp uint32 `json:"observations_timestamp,omitempty"` + ObserverCount uint8 `json:"observer_count,omitempty"` + Observers [19]uint8 `json:"observers,omitempty"` + JuelsPerLamport uint64 `json:"juels_per_lamport,omitempty"` + ReimbursementGJuels uint64 `json:"reimbursement_gjuels,omitempty"` +} + +// UnmarshalBinary makes NewTransmission implement encoding.BinaryUnmarshaler +func (n *NewTransmission) UnmarshalBinary(data []byte) error { + return bin.NewBinDecoder(data).Decode(n) +} diff --git a/pkg/monitoring/mocks/ChainReader.go b/pkg/monitoring/mocks/ChainReader.go index 75d36d13f..d1d9498d4 100644 --- a/pkg/monitoring/mocks/ChainReader.go +++ b/pkg/monitoring/mocks/ChainReader.go @@ -146,6 +146,29 @@ func (_m *ChainReader) GetTokenAccountBalance(ctx context.Context, account solan return r0, r1 } +// GetTransaction provides a mock function with given fields: ctx, txSig, opts +func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signature, opts *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error) { + ret := _m.Called(ctx, txSig, opts) + + var r0 *rpc.GetTransactionResult + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) *rpc.GetTransactionResult); ok { + r0 = rf(ctx, txSig, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.GetTransactionResult) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) error); ok { + r1 = rf(ctx, txSig, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // NewChainReader creates a new instance of ChainReader. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewChainReader(t testing.TB) *ChainReader { mock := &ChainReader{} diff --git a/pkg/monitoring/source_envelope.go b/pkg/monitoring/source_envelope.go index ce8da30cb..13b0c36fd 100644 --- a/pkg/monitoring/source_envelope.go +++ b/pkg/monitoring/source_envelope.go @@ -7,8 +7,10 @@ import ( "sync" "time" + "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" relayMonitoring "github.com/smartcontractkit/chainlink-relay/pkg/monitoring" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/event" pkgSolana "github.com/smartcontractkit/chainlink-solana/pkg/solana" "github.com/smartcontractkit/libocr/offchainreporting2/types" "go.uber.org/multierr" @@ -70,13 +72,12 @@ func (s *envelopeSource) Fetch(ctx context.Context) (interface{}, error) { // extra BlockNumber: blockNum, Transmitter: types.Account(state.Config.LatestTransmitter.String()), - JuelsPerFeeCoin: big.NewInt(0), // TODO (dru) AggregatorRoundID: state.Config.LatestAggregatorRoundID, } var envelopeErr error envelopeMu := &sync.Mutex{} wg := &sync.WaitGroup{} - wg.Add(2) + wg.Add(3) go func() { defer wg.Done() answer, _, transmissionErr := s.client.GetLatestTransmission(ctx, state.Transmissions, rpc.CommitmentConfirmed) @@ -109,6 +110,17 @@ func (s *envelopeSource) Fetch(ctx context.Context) (interface{}, error) { } envelope.LinkBalance = linkBalance }() + go func() { + defer wg.Done() + juelsPerLamport, juelsErr := s.getJuelsPerLamport(ctx) + envelopeMu.Lock() + defer envelopeMu.Unlock() + if juelsErr != nil { + envelopeErr = multierr.Combine(envelopeErr, fmt.Errorf("Failed to fetch Juels/FeeCoin: %w", juelsErr)) + return + } + envelope.JuelsPerFeeCoin = juelsPerLamport + }() wg.Wait() if envelopeErr != nil { return nil, envelopeErr @@ -121,6 +133,54 @@ func (s *envelopeSource) Fetch(ctx context.Context) (interface{}, error) { return envelope, nil } +func (s *envelopeSource) getJuelsPerLamport(ctx context.Context) (*big.Int, error) { + txSigsPageSize := 30 + txSigs, err := s.client.GetSignaturesForAddressWithOpts( + ctx, + s.feedConfig.StateAccount, + &rpc.GetSignaturesForAddressOpts{ + Commitment: rpc.CommitmentConfirmed, + Limit: &txSigsPageSize, // we only need the last tx + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch tx signatures for state account '%s': %w", s.feedConfig.StateAccountBase58, err) + } + if len(txSigs) == 0 { + return nil, fmt.Errorf("found no transactions from state account '%s'", s.feedConfig.StateAccountBase58) + } + for _, txSig := range txSigs { + if txSig.Err != nil { + continue + } + txRes, err := s.client.GetTransaction( + ctx, + txSig.Signature, + &rpc.GetTransactionOpts{ + Commitment: rpc.CommitmentConfirmed, + Encoding: solana.EncodingBase64, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch tx with signature %s: %w", txSig.Signature, err) + } + if txRes == nil { + return nil, fmt.Errorf("no transaction returned for signature %s", txSig.Signature) + } + events := event.ExtractEvents(txRes.Meta.LogMessages, s.feedConfig.ContractAddressBase58) + for _, rawEvent := range events { + decodedEvent, err := event.Decode(rawEvent) + if err != nil { + return nil, fmt.Errorf("failed decode events '%s' from tx with signature '%s': %w", rawEvent, txSigs[0].Signature, err) + } + if newTransmission, isNewTransmission := decodedEvent.(event.NewTransmission); isNewTransmission { + return new(big.Int).SetUint64(newTransmission.JuelsPerLamport), nil + } + } + } + return nil, fmt.Errorf("no NewTransmission event found in the last %d transactions on contract state '%s'", txSigsPageSize, s.feedConfig.StateAccountBase58) +} + // Helpers func getLinkAvailableForPayment(state pkgSolana.State, linkBalance *big.Int) (*big.Int, error) { diff --git a/pkg/monitoring/source_envelope_test.go b/pkg/monitoring/source_envelope_test.go index 6c88bf954..104d4e5b4 100644 --- a/pkg/monitoring/source_envelope_test.go +++ b/pkg/monitoring/source_envelope_test.go @@ -90,6 +90,25 @@ var ( Data: big.NewInt(51268930158), Timestamp: 0x627116e9, } + fakeTxSignatures = []*rpc.TransactionSignature{ + { + Err: interface{}(nil), + Signature: solana.Signature{0x24, 0xc, 0x70, 0x3b, 0xd4, 0xdd, 0x90, 0xd, 0x41, 0xe8, 0x25, 0xe0, 0xbe, 0x4c, 0x12, 0xe5, 0x25, 0x9a, 0x44, 0x8a, 0xd0, 0x66, 0x95, 0x57, 0xfc, 0xbc, 0xc9, 0x7e, 0x7f, 0xc6, 0x72, 0x30, 0xf0, 0x9e, 0x8a, 0x5a, 0x6f, 0xe1, 0x75, 0x1b, 0xdb, 0xef, 0xc0, 0x16, 0x27, 0xa9, 0xfb, 0x22, 0xe7, 0x85, 0xfb, 0x71, 0x66, 0x9b, 0xdb, 0xca, 0x9e, 0x98, 0x88, 0xb1, 0x98, 0xc5, 0x30, 0x6}, + }, + } + fakeTxResult = &rpc.GetTransactionResult{ + Meta: &rpc.TransactionMeta{ + LogMessages: []string{ + "Program cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ invoke [1]", + "Program data: gjbLTR5rT6jBkhUAAAOEYez4J1V9kiJLU33Kl+nEg3cYc/gpj1gUdhFTZIlARYTZtgEAAAAAAAAAAAAABrMErmIOBA8JBgELCAUDDA4CDQcAAAAAAPuxKzMBAAAAp2QAAAAAAAA=", "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny invoke [2]", + "Program log: Instruction: Submit", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny consumed 3467 of 1212618 compute units", + "Program HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny success", + "Program cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ consumed 191409 of 1400000 compute units", + "Program cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ success", + }, + }, + } ) func TestEnvelopeSource(t *testing.T) { @@ -98,6 +117,8 @@ func TestEnvelopeSource(t *testing.T) { // Generated data. chainConfig := generateChainConfig() feedConfig := generateFeedConfig() + feedConfig.ContractAddressBase58 = "cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ" + feedConfig.ContractAddress = solana.MustPublicKeyFromBase58(feedConfig.ContractAddressBase58) // Setup mocks chainReader := new(mocks.ChainReader) @@ -117,6 +138,19 @@ func TestEnvelopeSource(t *testing.T) { fakeState.Config.TokenVault, rpc.CommitmentConfirmed, ).Return(fakeLinkBalanceRes, nil).Once() + chainReader.On("GetSignaturesForAddressWithOpts", + mock.Anything, // ctx + feedConfig.StateAccount, + mock.Anything, // // because it's hard to mock pointer values! + ).Return(fakeTxSignatures, nil) + chainReader.On("GetTransaction", + mock.Anything, // ctx + fakeTxSignatures[0].Signature, + &rpc.GetTransactionOpts{ + Commitment: rpc.CommitmentConfirmed, + Encoding: solana.EncodingBase64, + }, + ).Return(fakeTxResult, nil) // Call Fetch factory := NewEnvelopeSourceFactory(chainReader, newNullLogger()) @@ -181,7 +215,7 @@ func TestEnvelopeSource(t *testing.T) { Transmitter: "MovNQPmSSdyD8deQH6qhdoEQ57tTKPEMsv6w6LXmo1h", LinkBalance: big.NewInt(824640212736), LinkAvailableForPayment: big.NewInt(818267512487), - JuelsPerFeeCoin: big.NewInt(0), + JuelsPerFeeCoin: big.NewInt(5153468923), AggregatorRoundID: 0x13841a, } require.Equal(t, expectedEnvelope, envelope)