diff --git a/.github/workflows/node.test.yaml b/.github/workflows/node.test.yaml index 9c2025b8b..4ee1e267b 100644 --- a/.github/workflows/node.test.yaml +++ b/.github/workflows/node.test.yaml @@ -95,4 +95,4 @@ jobs: ETH_PROVIDER_URL: "https://ethereum-sepolia-rpc.allthatnode.com" ETH_REPORTER_PK: ${{ secrets.TEST_DELEGATOR_REPORTER_PK}} TEST_FEE_PAYER_PK: ${{ secrets.DELEGATOR_FEEPAYER_PK}} - SUBMISSION_PROXY_CONTRACT: "0x8B5B98ABdc0281D5cf8bD93FE82768b22FD11623" + SUBMISSION_PROXY_CONTRACT: "0x63Fc20a60438adD9B10F94E218c16561a7827eB4" diff --git a/node/migrations/000013_proofs.down.sql b/node/migrations/000013_proofs.down.sql new file mode 100644 index 000000000..2d26d087d --- /dev/null +++ b/node/migrations/000013_proofs.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS proofs; \ No newline at end of file diff --git a/node/migrations/000013_proofs.up.sql b/node/migrations/000013_proofs.up.sql new file mode 100644 index 000000000..2bc8c4e46 --- /dev/null +++ b/node/migrations/000013_proofs.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS proofs ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + round INT8 NOT NULL, + proof BYTEA +) \ No newline at end of file diff --git a/node/pkg/aggregator/aggregator.go b/node/pkg/aggregator/aggregator.go index c92723c1c..da42d6d42 100644 --- a/node/pkg/aggregator/aggregator.go +++ b/node/pkg/aggregator/aggregator.go @@ -4,11 +4,11 @@ import ( "context" "encoding/json" "fmt" - "strings" "sync" "time" + "bisonai.com/orakl/node/pkg/chain/helper" "bisonai.com/orakl/node/pkg/db" "bisonai.com/orakl/node/pkg/raft" "bisonai.com/orakl/node/pkg/utils/calculator" @@ -30,11 +30,19 @@ func NewAggregator(h host.Host, ps *pubsub.PubSub, topicString string) (*Aggrega return nil, err } + signHelper, err := helper.NewSignHelper("") + if err != nil { + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to create sign helper") + return nil, err + } + aggregator := Aggregator{ Raft: raft.NewRaftNode(h, ps, topic, 100, LEADER_TIMEOUT), CollectedPrices: map[int64][]int64{}, + CollectedProofs: map[int64][][]byte{}, AggregatorMutex: sync.Mutex{}, RoundID: 0, + SignHelper: signHelper, } aggregator.Raft.LeaderJob = aggregator.LeaderJob aggregator.Raft.HandleCustomMessage = aggregator.HandleCustomMessage @@ -54,24 +62,7 @@ func (n *Aggregator) Run(ctx context.Context) { func (n *Aggregator) LeaderJob() error { n.RoundID++ n.Raft.IncreaseTerm() - roundMessage := RoundSyncMessage{ - LeaderID: n.Raft.Host.ID().String(), - RoundID: n.RoundID, - } - - marshalledRoundMessage, err := json.Marshal(roundMessage) - if err != nil { - log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to marshal round message") - return err - } - - message := raft.Message{ - Type: RoundSync, - SentFrom: n.Raft.Host.ID().String(), - Data: json.RawMessage(marshalledRoundMessage), - } - - return n.Raft.PublishMessage(message) + return n.PublishRoundMessage(n.RoundID) } func (n *Aggregator) HandleCustomMessage(message raft.Message) error { @@ -80,6 +71,8 @@ func (n *Aggregator) HandleCustomMessage(message raft.Message) error { return n.HandleRoundSyncMessage(message) case PriceData: return n.HandlePriceDataMessage(message) + case ProofMsg: + return n.HandleProofMessage(message) default: return fmt.Errorf("unknown message type received in HandleCustomMessage: %v", message.Type) } @@ -100,30 +93,14 @@ func (n *Aggregator) HandleRoundSyncMessage(msg raft.Message) error { n.RoundID = roundSyncMessage.RoundID } var updateValue int64 = -1 - value, updateTime, err := n.getLatestLocalAggregate(n.nodeCtx) + value, updateTime, err := GetLatestLocalAggregate(n.nodeCtx, n.Name) if err == nil && n.LastLocalAggregateTime.IsZero() || !n.LastLocalAggregateTime.Equal(updateTime) { updateValue = value n.LastLocalAggregateTime = updateTime } - priceDataMessage := PriceDataMessage{ - RoundID: n.RoundID, - PriceData: updateValue, - } - - marshalledPriceDataMessage, err := json.Marshal(priceDataMessage) - if err != nil { - return err - } - - message := raft.Message{ - Type: PriceData, - SentFrom: n.Raft.Host.ID().String(), - Data: json.RawMessage(marshalledPriceDataMessage), - } - - return n.Raft.PublishMessage(message) + return n.PublishPriceDataMessage(n.RoundID, updateValue) } func (n *Aggregator) HandlePriceDataMessage(msg raft.Message) error { @@ -154,25 +131,49 @@ func (n *Aggregator) HandlePriceDataMessage(msg raft.Message) error { return err } log.Debug().Str("Player", "Aggregator").Int64("roundId", priceDataMessage.RoundID).Int64("global_aggregate", median).Msg("global aggregated") - err = n.insertGlobalAggregate(median, priceDataMessage.RoundID) + err = InsertGlobalAggregate(n.nodeCtx, n.Name, median, priceDataMessage.RoundID) if err != nil { log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to insert global aggregate") return err } + + proof, err := n.SignHelper.MakeGlobalAggregateProof(median) + if err != nil { + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to make global aggregate proof") + return err + } + return n.PublishProofMessage(priceDataMessage.RoundID, proof) } return nil } -func (n *Aggregator) getLatestLocalAggregate(ctx context.Context) (int64, time.Time, error) { - redisAggregate, err := GetLatestLocalAggregateFromRdb(ctx, n.Name) +func (n *Aggregator) HandleProofMessage(msg raft.Message) error { + var proofMessage ProofMessage + err := json.Unmarshal(msg.Data, &proofMessage) if err != nil { - pgsqlAggregate, err := GetLatestLocalAggregateFromPgs(ctx, n.Name) + return err + } + + if proofMessage.RoundID == 0 { + return fmt.Errorf("invalid proof message: %v", proofMessage) + } + + n.AggregatorMutex.Lock() + defer n.AggregatorMutex.Unlock() + if _, ok := n.CollectedProofs[proofMessage.RoundID]; !ok { + n.CollectedProofs[proofMessage.RoundID] = [][]byte{} + } + + n.CollectedProofs[proofMessage.RoundID] = append(n.CollectedProofs[proofMessage.RoundID], proofMessage.Proof) + if len(n.CollectedProofs[proofMessage.RoundID]) >= n.Raft.SubscribersCount()+1 { + defer delete(n.CollectedProofs, proofMessage.RoundID) + err := InsertProof(n.nodeCtx, n.Name, proofMessage.RoundID, n.CollectedProofs[proofMessage.RoundID]) if err != nil { - return 0, time.Time{}, err + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to insert proof") + return err } - return pgsqlAggregate.Value, pgsqlAggregate.Timestamp, nil } - return redisAggregate.Value, redisAggregate.Timestamp, nil + return nil } func (n *Aggregator) getLatestRoundId(ctx context.Context) (int64, error) { @@ -183,43 +184,67 @@ func (n *Aggregator) getLatestRoundId(ctx context.Context) (int64, error) { return result.Round, nil } -func (n *Aggregator) insertGlobalAggregate(value int64, round int64) error { - var errs []string +func (n *Aggregator) executeDeviation() error { + // signals for deviation job which triggers immediate aggregation and sends submission request to submitter + return nil +} - err := n.insertPgsql(n.nodeCtx, value, round) - if err != nil { - log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to insert global aggregate into pgsql") - errs = append(errs, err.Error()) +func (n *Aggregator) PublishRoundMessage(roundId int64) error { + roundMessage := RoundSyncMessage{ + LeaderID: n.Raft.GetHostId(), + RoundID: roundId, } - err = n.insertRdb(n.nodeCtx, value, round) + marshalledRoundMessage, err := json.Marshal(roundMessage) if err != nil { - log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to insert global aggregate into rdb") - errs = append(errs, err.Error()) + return err } - if len(errs) > 0 { - return fmt.Errorf(strings.Join(errs, "; ")) + message := raft.Message{ + Type: RoundSync, + SentFrom: n.Raft.GetHostId(), + Data: json.RawMessage(marshalledRoundMessage), } - return nil + return n.Raft.PublishMessage(message) } -func (n *Aggregator) insertPgsql(ctx context.Context, value int64, round int64) error { - return db.QueryWithoutResult(ctx, InsertGlobalAggregateQuery, map[string]any{"name": n.Name, "value": value, "round": round}) -} +func (n *Aggregator) PublishPriceDataMessage(roundId int64, value int64) error { + priceDataMessage := PriceDataMessage{ + RoundID: roundId, + PriceData: value, + } -func (n *Aggregator) insertRdb(ctx context.Context, value int64, round int64) error { - key := "globalAggregate:" + n.Name - data, err := json.Marshal(globalAggregate{Name: n.Name, Value: value, Round: round, Timestamp: time.Now()}) + marshalledPriceDataMessage, err := json.Marshal(priceDataMessage) if err != nil { - log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to marshal global aggregate") return err } - return db.Set(ctx, key, string(data), time.Duration(5*time.Minute)) + + message := raft.Message{ + Type: PriceData, + SentFrom: n.Raft.GetHostId(), + Data: json.RawMessage(marshalledPriceDataMessage), + } + + return n.Raft.PublishMessage(message) } -func (n *Aggregator) executeDeviation() error { - // signals for deviation job which triggers immediate aggregation and sends submission request to submitter - return nil +func (n *Aggregator) PublishProofMessage(roundId int64, proof []byte) error { + proofMessage := ProofMessage{ + RoundID: roundId, + Proof: proof, + } + + marshalledProofMessage, err := json.Marshal(proofMessage) + if err != nil { + return err + } + + message := raft.Message{ + Type: ProofMsg, + SentFrom: n.Raft.GetHostId(), + Data: json.RawMessage(marshalledProofMessage), + } + + return n.Raft.PublishMessage(message) } diff --git a/node/pkg/aggregator/aggregator_test.go b/node/pkg/aggregator/aggregator_test.go index 4e59b5785..5f94a46b5 100644 --- a/node/pkg/aggregator/aggregator_test.go +++ b/node/pkg/aggregator/aggregator_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "bisonai.com/orakl/node/pkg/db" "github.com/stretchr/testify/assert" ) @@ -76,7 +77,7 @@ func TestGetLatestLocalAggregate(t *testing.T) { node.Name = "test_pair" - val, dbTime, err := node.getLatestLocalAggregate(ctx) + val, dbTime, err := GetLatestLocalAggregate(ctx, node.Name) if err != nil { t.Fatal("error getting latest local aggregate") } @@ -136,7 +137,7 @@ func TestInsertGlobalAggregate(t *testing.T) { node.Name = "test_pair" - err = node.insertGlobalAggregate(20, 2) + err = InsertGlobalAggregate(ctx, node.Name, 20, 2) if err != nil { t.Fatal("error inserting global aggregate") } @@ -146,12 +147,62 @@ func TestInsertGlobalAggregate(t *testing.T) { t.Fatal("error getting latest round id") } - redisResult, err := GetLatestGlobalAggregateFromRdb(ctx, "test_pair") + redisResult, err := getLatestGlobalAggregateFromRdb(ctx, "test_pair") if err != nil { t.Fatal("error getting latest global aggregate from rdb") } assert.Equal(t, int64(20), redisResult.Value) assert.Equal(t, int64(2), redisResult.Round) - assert.Equal(t, int64(2), roundId) } + +func TestInsertProof(t *testing.T) { + ctx := context.Background() + cleanup, testItems, err := setup(ctx) + if err != nil { + t.Fatalf("error setting up test: %v", err) + } + defer func() { + if cleanupErr := cleanup(); cleanupErr != nil { + t.Logf("Cleanup failed: %v", cleanupErr) + } + }() + + node, err := NewAggregator(testItems.app.Host, testItems.app.Pubsub, testItems.topicString) + if err != nil { + t.Fatal("error creating new node") + } + + node.Name = "test_pair" + + value := int64(20) + round := int64(2) + p, err := node.SignHelper.MakeGlobalAggregateProof(value) + if err != nil { + t.Fatal("error making global aggregate proof") + } + + err = InsertProof(ctx, node.Name, round, [][]byte{p, p}) + if err != nil { + t.Fatal("error inserting proof") + } + + rdbResult, err := getProofFromRdb(ctx, node.Name, round) + if err != nil { + t.Fatal("error getting proof from rdb") + } + + assert.EqualValues(t, concatBytes([][]byte{p, p}), rdbResult.Proof) + + pgsqlResult, err := getProofFromPgsql(ctx, node.Name, round) + if err != nil { + t.Fatal("error getting proof from pgsql:" + err.Error()) + } + + assert.EqualValues(t, concatBytes([][]byte{p, p}), pgsqlResult.Proof) + + err = db.QueryWithoutResult(ctx, "DELETE FROM proofs", nil) + if err != nil { + t.Fatal("error deleting proofs") + } +} diff --git a/node/pkg/aggregator/types.go b/node/pkg/aggregator/types.go index 4b4fb144c..b498ad871 100644 --- a/node/pkg/aggregator/types.go +++ b/node/pkg/aggregator/types.go @@ -6,6 +6,7 @@ import ( "time" "bisonai.com/orakl/node/pkg/bus" + "bisonai.com/orakl/node/pkg/chain/helper" "bisonai.com/orakl/node/pkg/raft" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/host" @@ -14,10 +15,12 @@ import ( const ( RoundSync raft.MessageType = "roundSync" PriceData raft.MessageType = "priceData" + ProofMsg raft.MessageType = "proof" SelectActiveAggregatorsQuery = `SELECT * FROM aggregators WHERE active = true` SelectLatestLocalAggregateQuery = `SELECT * FROM local_aggregates WHERE name = @name ORDER BY timestamp DESC LIMIT 1` InsertGlobalAggregateQuery = `INSERT INTO global_aggregates (name, value, round) VALUES (@name, @value, @round) RETURNING *` SelectLatestGlobalAggregateQuery = `SELECT * FROM global_aggregates WHERE name = @name ORDER BY round DESC LIMIT 1` + InsertProofQuery = `INSERT INTO proofs (name, round, proof) VALUES (@name, @round, @proof) RETURNING *` ) type redisLocalAggregate struct { @@ -25,6 +28,20 @@ type redisLocalAggregate struct { Timestamp time.Time `json:"timestamp"` } +// pgsql row entry +type PgsqlProof struct { + ID int64 `db:"id"` + Name string `json:"name"` + Round int64 `json:"round"` + Proof []byte `json:"proof"` +} + +type Proof struct { + Name string `json:"name"` + Round int64 `json:"round"` + Proof []byte `json:"proofs"` +} + type pgsLocalAggregate struct { Name string `db:"name"` Value int64 `db:"value"` @@ -56,11 +73,14 @@ type Aggregator struct { Raft *raft.Raft CollectedPrices map[int64][]int64 + CollectedProofs map[int64][][]byte AggregatorMutex sync.Mutex LastLocalAggregateTime time.Time RoundID int64 + SignHelper *helper.SignHelper + nodeCtx context.Context nodeCancel context.CancelFunc isRunning bool @@ -75,3 +95,8 @@ type PriceDataMessage struct { RoundID int64 `json:"roundID"` PriceData int64 `json:"priceData"` } + +type ProofMessage struct { + RoundID int64 `json:"roundID"` + Proof []byte `json:"proof"` +} diff --git a/node/pkg/aggregator/utils.go b/node/pkg/aggregator/utils.go index c165c86b5..c3eb93245 100644 --- a/node/pkg/aggregator/utils.go +++ b/node/pkg/aggregator/utils.go @@ -3,8 +3,13 @@ package aggregator import ( "context" "encoding/json" + "fmt" + "strconv" + "strings" + "time" "bisonai.com/orakl/node/pkg/db" + "github.com/rs/zerolog/log" ) func GetLatestLocalAggregateFromRdb(ctx context.Context, name string) (redisLocalAggregate, error) { @@ -26,7 +31,137 @@ func GetLatestLocalAggregateFromPgs(ctx context.Context, name string) (pgsLocalA return db.QueryRow[pgsLocalAggregate](ctx, SelectLatestLocalAggregateQuery, map[string]any{"name": name}) } -func GetLatestGlobalAggregateFromRdb(ctx context.Context, name string) (globalAggregate, error) { +func FilterNegative(values []int64) []int64 { + result := []int64{} + for _, value := range values { + if value < 0 { + continue + } + result = append(result, value) + } + return result +} + +func InsertGlobalAggregate(ctx context.Context, name string, value int64, round int64) error { + var errs []string + + err := insertRdb(ctx, name, value, round) + if err != nil { + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to insert global aggregate into rdb") + errs = append(errs, err.Error()) + } + + err = insertPgsql(ctx, name, value, round) + if err != nil { + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to insert global aggregate into pgsql") + errs = append(errs, err.Error()) + } + + if len(errs) > 0 { + return fmt.Errorf(strings.Join(errs, "; ")) + } + + return nil +} + +func insertPgsql(ctx context.Context, name string, value int64, round int64) error { + return db.QueryWithoutResult(ctx, InsertGlobalAggregateQuery, map[string]any{"name": name, "value": value, "round": round}) +} + +func insertRdb(ctx context.Context, name string, value int64, round int64) error { + key := "globalAggregate:" + name + data, err := json.Marshal(globalAggregate{Name: name, Value: value, Round: round}) + if err != nil { + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to marshal global aggregate") + return err + } + return db.Set(ctx, key, string(data), time.Duration(5*time.Minute)) +} + +func InsertProof(ctx context.Context, name string, round int64, proofs [][]byte) error { + var errs []string + + err := insertProofRdb(ctx, name, round, proofs) + if err != nil { + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to insert proof into rdb") + errs = append(errs, err.Error()) + } + + err = insertProofPgsql(ctx, name, round, proofs) + if err != nil { + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to insert proof into pgsql") + errs = append(errs, err.Error()) + } + + if len(errs) > 0 { + return fmt.Errorf(strings.Join(errs, "; ")) + } + + return nil +} + +func insertProofPgsql(ctx context.Context, name string, round int64, proofs [][]byte) error { + concatProof := concatBytes(proofs) + err := db.QueryWithoutResult(ctx, InsertProofQuery, map[string]any{"name": name, "round": round, "proof": concatProof}) + if err != nil { + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to insert proofs into pgsql") + } + + return err +} + +func insertProofRdb(ctx context.Context, name string, round int64, proofs [][]byte) error { + concatProof := concatBytes(proofs) + key := "proof:" + name + "|round:" + strconv.FormatInt(round, 10) + data, err := json.Marshal(Proof{Name: name, Round: round, Proof: concatProof}) + if err != nil { + log.Error().Str("Player", "Aggregator").Err(err).Msg("failed to marshal proofs") + return err + } + return db.Set(ctx, key, string(data), time.Duration(5*time.Minute)) +} + +func GetLatestLocalAggregate(ctx context.Context, name string) (int64, time.Time, error) { + redisAggregate, err := GetLatestLocalAggregateFromRdb(ctx, name) + if err != nil { + pgsqlAggregate, err := GetLatestLocalAggregateFromPgs(ctx, name) + if err != nil { + return 0, time.Time{}, err + } + return pgsqlAggregate.Value, pgsqlAggregate.Timestamp, nil + } + return redisAggregate.Value, redisAggregate.Timestamp, nil +} + +// used for testing +func getProofFromRdb(ctx context.Context, name string, round int64) (Proof, error) { + key := "proof:" + name + "|round:" + strconv.FormatInt(round, 10) + var proofs Proof + data, err := db.Get(ctx, key) + if err != nil { + return proofs, err + } + + err = json.Unmarshal([]byte(data), &proofs) + if err != nil { + return proofs, err + } + return proofs, nil +} + +// used for testing +func getProofFromPgsql(ctx context.Context, name string, round int64) (Proof, error) { + rawProof, err := db.QueryRow[PgsqlProof](ctx, "SELECT * FROM proofs WHERE name = @name AND round = @round", map[string]any{"name": name, "round": round}) + if err != nil { + return Proof{}, err + } + + proofs := Proof{Name: name, Round: round, Proof: rawProof.Proof} + return proofs, nil +} + +// used for testing +func getLatestGlobalAggregateFromRdb(ctx context.Context, name string) (globalAggregate, error) { key := "globalAggregate:" + name var aggregate globalAggregate data, err := db.Get(ctx, key) @@ -41,13 +176,10 @@ func GetLatestGlobalAggregateFromRdb(ctx context.Context, name string) (globalAg return aggregate, nil } -func FilterNegative(values []int64) []int64 { - result := []int64{} - for _, value := range values { - if value < 0 { - continue - } - result = append(result, value) +func concatBytes(slices [][]byte) []byte { + var result []byte + for _, slice := range slices { + result = append(result, slice...) } return result } diff --git a/node/pkg/chain/helper/helper.go b/node/pkg/chain/helper/helper.go index 0e3004382..a9a243181 100644 --- a/node/pkg/chain/helper/helper.go +++ b/node/pkg/chain/helper/helper.go @@ -2,6 +2,7 @@ package helper import ( "context" + "crypto/ecdsa" "errors" "math/big" "os" @@ -24,6 +25,10 @@ type ChainHelper struct { lastUsedWalletIndex int } +type SignHelper struct { + PK *ecdsa.PrivateKey +} + type signedTx struct { SignedRawTx *string `json:"signedRawTx" db:"signedRawTx"` } @@ -267,3 +272,27 @@ func (t *ChainHelper) retryOnJsonRpcFailure(ctx context.Context, job func(c util } return nil } + +func NewSignHelper(pk string) (*SignHelper, error) { + if pk == "" { + pk = os.Getenv(KlaytnReporterPk) + if pk == "" { + log.Error().Msg("reporter pk not set") + return nil, errors.New("reporter pk not set") + } + } + + pk = strings.TrimPrefix(pk, "0x") + privateKey, err := utils.StringToPk(pk) + if err != nil { + log.Error().Err(err).Msg("failed to convert pk") + return nil, err + } + return &SignHelper{ + PK: privateKey, + }, nil +} + +func (s *SignHelper) MakeGlobalAggregateProof(val int64) ([]byte, error) { + return utils.MakeValueSignature(val, s.PK) +} diff --git a/node/pkg/chain/tests/chain_test.go b/node/pkg/chain/tests/chain_test.go index 2ba681cd8..2e8a7f83a 100644 --- a/node/pkg/chain/tests/chain_test.go +++ b/node/pkg/chain/tests/chain_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "math/big" + "os" "strings" "testing" @@ -12,6 +13,7 @@ import ( "bisonai.com/orakl/node/pkg/chain/utils" "bisonai.com/orakl/node/pkg/db" "github.com/klaytn/klaytn/blockchain/types" + "github.com/klaytn/klaytn/crypto" "github.com/stretchr/testify/assert" ) @@ -53,7 +55,13 @@ func TestNewKlaytnHelper(t *testing.T) { if err != nil { t.Errorf("Unexpected error: %v", err) } +} +func TestNewChainHelper(t *testing.T) { + _, err := helper.NewSignHelper("") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } } func TestNewEthHelper(t *testing.T) { @@ -469,3 +477,31 @@ func TestMakeAbiFuncAttribute(t *testing.T) { } } } + +func TestMakeGlobalAggregateProof(t *testing.T) { + s, err := helper.NewSignHelper("") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + proof, err := s.MakeGlobalAggregateProof(200000000) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + assert.NotEqual(t, proof, nil) + + hash := utils.Value2HashForSign(200000000) + addr, err := utils.RecoverSigner(hash, proof) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + pk, err := utils.StringToPk(os.Getenv("KLAYTN_REPORTER_PK")) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + addrFromEnv := crypto.PubkeyToAddress(pk.PublicKey) + + assert.Equal(t, addrFromEnv.Hex(), addr.Hex()) +} diff --git a/node/pkg/chain/utils/utils.go b/node/pkg/chain/utils/utils.go index b161951a5..23af3dfbd 100644 --- a/node/pkg/chain/utils/utils.go +++ b/node/pkg/chain/utils/utils.go @@ -120,6 +120,7 @@ func generateABI(functionName string, inputs string, outputs string, stateMutabi parsedABI, err := abi.JSON(strings.NewReader(json)) if err != nil { + log.Error().Err(err).Msg("failed to parse abi") return nil, err } @@ -172,11 +173,13 @@ func MakeDirectTx(ctx context.Context, client ClientInterface, contractAddressHe abi, err := GenerateCallABI(functionName, inputs, outputs) if err != nil { + log.Error().Err(err).Msg("failed to generate abi") return nil, err } packed, err := abi.Pack(functionName, args...) if err != nil { + log.Error().Err(err).Msg("failed to pack abi") return nil, err } @@ -540,3 +543,47 @@ func IsJsonRpcFailureError(errorCode int) bool { } return false } + +func MakeValueSignature(value int64, pk *ecdsa.PrivateKey) ([]byte, error) { + hash := Value2HashForSign(value) + signature, err := crypto.Sign(hash, pk) + if err != nil { + return nil, err + } + + // Convert V from 0/1 to 27/28 + if signature[64] < 27 { + signature[64] += 27 + } + + return signature, nil +} + +func Value2HashForSign(value int64) []byte { + bigIntVal := big.NewInt(value) + buf := make([]byte, 32) + + copy(buf[32-len(bigIntVal.Bytes()):], bigIntVal.Bytes()) + return crypto.Keccak256(buf) +} + +func StringToPk(pk string) (*ecdsa.PrivateKey, error) { + return crypto.HexToECDSA(strings.TrimPrefix(pk, "0x")) +} + +func RecoverSigner(hash []byte, signature []byte) (address common.Address, err error) { + if len(signature) != 65 { + return common.Address{}, fmt.Errorf("signature must be 65 bytes long") + } + + signature[64] -= 27 + + pubKey, err := crypto.SigToPub(hash, signature) + if err != nil { + return common.Address{}, err + } + + address = crypto.PubkeyToAddress(*pubKey) + + return address, nil +} diff --git a/node/pkg/reporter/main_test.go b/node/pkg/reporter/main_test.go index 2d6743f30..966853ea8 100644 --- a/node/pkg/reporter/main_test.go +++ b/node/pkg/reporter/main_test.go @@ -12,6 +12,7 @@ import ( "bisonai.com/orakl/node/pkg/admin/reporter" "bisonai.com/orakl/node/pkg/admin/utils" "bisonai.com/orakl/node/pkg/bus" + "bisonai.com/orakl/node/pkg/chain/helper" "bisonai.com/orakl/node/pkg/db" libp2p_setup "bisonai.com/orakl/node/pkg/libp2p/setup" "github.com/gofiber/fiber/v2" @@ -31,11 +32,17 @@ type TestItems struct { type TmpData struct { globalAggregate GlobalAggregate submissionAddress SubmissionAddress + proofBytes []byte } func insertSampleData(ctx context.Context) (*TmpData, error) { var tmpData = new(TmpData) + signHelper, err := helper.NewSignHelper("") + if err != nil { + return nil, err + } + key := "globalAggregate:" + "test-aggregate" data, err := json.Marshal(map[string]any{"name": "test-aggregate", "value": int64(15), "round": int64(1)}) if err != nil { @@ -59,6 +66,32 @@ func insertSampleData(ctx context.Context) (*TmpData, error) { } tmpData.globalAggregate = tmpGlobalAggregate + rawProof, err := signHelper.MakeGlobalAggregateProof(int64(15)) + if err != nil { + return nil, err + } + tmpData.proofBytes = concatBytes([][]byte{rawProof, rawProof}) + + err = db.QueryWithoutResult(ctx, "INSERT INTO proofs (name, round, proof) VALUES (@name, @round, @proof)", map[string]any{"name": "test-aggregate", "round": int64(1), "proof": concatBytes([][]byte{rawProof, rawProof})}) + if err != nil { + return nil, err + } + + rdbProof := Proof{ + Name: "test-aggregate", + Round: int64(1), + Proof: concatBytes([][]byte{rawProof, rawProof}), + } + rdbProofData, err := json.Marshal(rdbProof) + if err != nil { + return nil, err + } + + err = db.Set(ctx, "proof:test-aggregate|round:1", string(rdbProofData), time.Duration(10*time.Second)) + if err != nil { + return nil, err + } + tmpAddress, err := db.QueryRow[SubmissionAddress](ctx, InsertAddressQuery, map[string]any{"name": "test-aggregate", "address": "0x1234", "interval": TestInterval}) if err != nil { return nil, err @@ -132,6 +165,11 @@ func reporterCleanup(ctx context.Context, admin *fiber.App, testItems *TestItems return err } + err = db.QueryWithoutResult(ctx, "DELETE FROM proofs;", nil) + if err != nil { + return err + } + err = admin.Shutdown() if err != nil { return err diff --git a/node/pkg/reporter/reporter.go b/node/pkg/reporter/reporter.go index f675bedf0..b0863efb4 100644 --- a/node/pkg/reporter/reporter.go +++ b/node/pkg/reporter/reporter.go @@ -99,11 +99,11 @@ func (r *Reporter) leaderJob() error { log.Warn().Str("Player", "Reporter").Msg("no valid aggregates to report") return nil } - log.Debug().Str("Player", "Reporter").Int("validAggregates", len(validAggregates)).Msg("valid aggregates") + err = r.report(ctx, validAggregates) if err != nil { - log.Error().Str("Player", "Reporter").Err(err).Msg("Report") + log.Error().Str("Player", "Reporter").Err(err).Msg("report") return err } @@ -114,6 +114,7 @@ func (r *Reporter) leaderJob() error { } log.Debug().Str("Player", "Reporter").Dur("duration", time.Since(start)).Msg("reporting done") return nil + } err := r.retry(job) @@ -126,6 +127,31 @@ func (r *Reporter) leaderJob() error { return nil } +func (r *Reporter) report(ctx context.Context, aggregates []GlobalAggregate) error { + proofMap, err := r.getProofsAsMap(ctx, aggregates) + if err != nil { + log.Error().Str("Player", "Reporter").Err(err).Msg("submit without proofs") + return r.reportWithoutProofs(ctx, aggregates) + } + + return r.reportWithProofs(ctx, aggregates, proofMap) +} + +func (r *Reporter) getProofsAsMap(ctx context.Context, aggregates []GlobalAggregate) (map[string][]byte, error) { + proofs, err := r.getProofs(ctx, aggregates) + if err != nil { + log.Error().Str("Player", "Reporter").Err(err).Msg("submit without proofs") + return nil, err + } + + if len(proofs) < len(aggregates) { + log.Error().Str("Player", "Reporter").Msg("proofs not found for all aggregates") + return nil, errors.New("proofs not found for all aggregates") + } + + return ProofsToMap(proofs), nil +} + func (r *Reporter) resignLeader() { r.Raft.StopHeartbeatTicker() r.Raft.UpdateRole(raft.Follower) @@ -186,28 +212,98 @@ func (r *Reporter) getLatestGlobalAggregatesRdb(ctx context.Context) ([]GlobalAg return aggregates, nil } -func (r *Reporter) report(ctx context.Context, aggregates []GlobalAggregate) error { - log.Debug().Str("Player", "Reporter").Int("aggregates", len(aggregates)).Msg("reporting") +func (r *Reporter) getProofs(ctx context.Context, aggregates []GlobalAggregate) ([]Proof, error) { + result, err := r.getProofsRdb(ctx, aggregates) + if err != nil { + log.Warn().Str("Player", "Reporter").Err(err).Msg("getProofsRdb failed, trying to get from pgsql") + return r.getProofsPgsql(ctx, aggregates) + } + return result, nil +} + +func (r *Reporter) getProofsRdb(ctx context.Context, aggregates []GlobalAggregate) ([]Proof, error) { + keys := make([]string, 0, len(aggregates)) + for _, agg := range aggregates { + keys = append(keys, "proof:"+agg.Name+"|round:"+strconv.FormatInt(agg.Round, 10)) + } + + result, err := db.MGet(ctx, keys) + if err != nil { + log.Error().Str("Player", "Reporter").Err(err).Msg("failed to get proofs") + return nil, err + } + + proofs := make([]Proof, 0, len(result)) + for i, proof := range result { + if proof == nil { + log.Error().Str("Player", "Reporter").Str("key", keys[i]).Msg("missing proof") + continue + } + var p Proof + err = json.Unmarshal([]byte(proof.(string)), &p) + if err != nil { + log.Error().Str("Player", "Reporter").Err(err).Str("key", keys[i]).Msg("failed to unmarshal proof") + continue + } + proofs = append(proofs, p) + + } + return proofs, nil +} + +func (r *Reporter) getProofsPgsql(ctx context.Context, aggregates []GlobalAggregate) ([]Proof, error) { + q := makeGetProofsQuery(aggregates) + rawResult, err := db.QueryRows[PgsqlProof](ctx, q, nil) + if err != nil { + log.Error().Str("Player", "Reporter").Err(err).Msg("failed to get proofs") + return nil, err + } + return convertPgsqlProofsToProofs(rawResult), nil +} + +func (r *Reporter) reportWithoutProofs(ctx context.Context, aggregates []GlobalAggregate) error { + log.Debug().Str("Player", "Reporter").Int("aggregates", len(aggregates)).Msg("reporting without proofs") if r.KlaytnHelper == nil { + log.Error().Str("Player", "Reporter").Msg("klaytn helper not set") return errors.New("klaytn helper not set") } - addresses, values, err := r.makeContractArgs(aggregates) + addresses, values, err := r.makeContractArgsWithoutProofs(aggregates) if err != nil { - log.Error().Str("Player", "Reporter").Err(err).Msg("makeContractArgs") + log.Error().Str("Player", "Reporter").Err(err).Msg("makeContractArgsWithoutProofs") return err } - err = r.reportDelegated(ctx, addresses, values) + err = r.reportDelegated(ctx, SUBMIT_WITHOUT_PROOFS, addresses, values) if err != nil { log.Error().Str("Player", "Reporter").Err(err).Msg("reporting directly") - return r.reportDirect(ctx, addresses, values) + return r.reportDirect(ctx, SUBMIT_WITHOUT_PROOFS, addresses, values) } return nil } -func (r *Reporter) reportDirect(ctx context.Context, args ...interface{}) error { - rawTx, err := r.KlaytnHelper.MakeDirectTx(ctx, r.contractAddress, FUNCTION_STRING, args...) +func (r *Reporter) reportWithProofs(ctx context.Context, aggregates []GlobalAggregate, proofMap map[string][]byte) error { + log.Debug().Str("Player", "Reporter").Int("aggregates", len(aggregates)).Msg("reporting with proofs") + if r.KlaytnHelper == nil { + return errors.New("klaytn helper not set") + } + + addresses, values, proofs, err := r.makeContractArgsWithProofs(aggregates, proofMap) + if err != nil { + log.Error().Str("Player", "Reporter").Err(err).Msg("makeContractArgsWithProofs") + return err + } + + err = r.reportDelegated(ctx, SUBMIT_WITH_PROOFS, addresses, values, proofs) + if err != nil { + log.Error().Str("Player", "Reporter").Err(err).Msg("reporting directly") + return r.reportDirect(ctx, SUBMIT_WITH_PROOFS, addresses, values, proofs) + } + return nil +} + +func (r *Reporter) reportDirect(ctx context.Context, functionString string, args ...interface{}) error { + rawTx, err := r.KlaytnHelper.MakeDirectTx(ctx, r.contractAddress, functionString, args...) if err != nil { log.Error().Str("Player", "Reporter").Err(err).Msg("MakeDirectTx") return err @@ -216,8 +312,8 @@ func (r *Reporter) reportDirect(ctx context.Context, args ...interface{}) error return r.KlaytnHelper.SubmitRawTx(ctx, rawTx) } -func (r *Reporter) reportDelegated(ctx context.Context, args ...interface{}) error { - rawTx, err := r.KlaytnHelper.MakeFeeDelegatedTx(ctx, r.contractAddress, FUNCTION_STRING, args...) +func (r *Reporter) reportDelegated(ctx context.Context, functionString string, args ...interface{}) error { + rawTx, err := r.KlaytnHelper.MakeFeeDelegatedTx(ctx, r.contractAddress, functionString, args...) if err != nil { log.Error().Str("Player", "Reporter").Err(err).Msg("MakeFeeDelegatedTx") return err @@ -250,7 +346,7 @@ func (r *Reporter) isAggValid(aggregate GlobalAggregate) bool { return aggregate.Round > lastSubmission } -func (r *Reporter) makeContractArgs(aggregates []GlobalAggregate) ([]common.Address, []*big.Int, error) { +func (r *Reporter) makeContractArgsWithoutProofs(aggregates []GlobalAggregate) ([]common.Address, []*big.Int, error) { addresses := make([]common.Address, len(aggregates)) values := make([]*big.Int, len(aggregates)) for i, agg := range aggregates { @@ -269,6 +365,29 @@ func (r *Reporter) makeContractArgs(aggregates []GlobalAggregate) ([]common.Addr return addresses, values, nil } +func (r *Reporter) makeContractArgsWithProofs(aggregates []GlobalAggregate, proofMap map[string][]byte) ([]common.Address, []*big.Int, [][]byte, error) { + addresses := make([]common.Address, len(aggregates)) + values := make([]*big.Int, len(aggregates)) + proofs := make([][]byte, len(aggregates)) + + for i, agg := range aggregates { + if agg.Name == "" || agg.Value < 0 { + log.Error().Str("Player", "Reporter").Str("name", agg.Name).Int64("value", agg.Value).Msg("skipping invalid aggregate") + return nil, nil, nil, errors.New("invalid aggregate exists") + } + addresses[i] = r.SubmissionPairs[agg.Name].Address + values[i] = big.NewInt(agg.Value) + proofs[i] = proofMap[agg.Name+"-"+strconv.FormatInt(agg.Round, 10)] + + } + + if len(addresses) == 0 || len(values) == 0 || len(proofs) == 0 { + return nil, nil, nil, errors.New("no valid aggregates") + } + + return addresses, values, proofs, nil +} + func (r *Reporter) SetKlaytnHelper(ctx context.Context) error { if r.KlaytnHelper != nil { r.KlaytnHelper.Close() @@ -291,3 +410,32 @@ func calculateJitter(baseTimeout time.Duration) time.Duration { jitter := time.Duration(n.Int64()) * time.Millisecond return baseTimeout + jitter } + +func convertPgsqlProofsToProofs(pgsqlProofs []PgsqlProof) []Proof { + proofs := make([]Proof, len(pgsqlProofs)) + for i, pgsqlProof := range pgsqlProofs { + proofs[i] = Proof{ + Name: pgsqlProof.Name, + Round: pgsqlProof.Round, + Proof: pgsqlProof.Proof, + } + } + return proofs +} + +func concatBytes(slices [][]byte) []byte { + var result []byte + for _, slice := range slices { + result = append(result, slice...) + } + return result +} + +func ProofsToMap(proofs []Proof) map[string][]byte { + m := make(map[string][]byte) + for _, proof := range proofs { + //m[name-round] = proof + m[proof.Name+"-"+strconv.FormatInt(proof.Round, 10)] = proof.Proof + } + return m +} diff --git a/node/pkg/reporter/reporter_test.go b/node/pkg/reporter/reporter_test.go index 0960e6ab5..c4d40d8e5 100644 --- a/node/pkg/reporter/reporter_test.go +++ b/node/pkg/reporter/reporter_test.go @@ -236,13 +236,35 @@ func TestMakeContractArgs(t *testing.T) { Value: 15, Round: 1, } - addresses, values, err := reporter.makeContractArgs([]GlobalAggregate{agg}) + + addresses, values, err := reporter.makeContractArgsWithoutProofs([]GlobalAggregate{agg}) if err != nil { t.Fatal("error making contract args") } assert.Equal(t, addresses[0], reporter.SubmissionPairs[agg.Name].Address) assert.Equal(t, values[0], big.NewInt(15)) + + rawProofs, err := reporter.getProofsRdb(ctx, []GlobalAggregate{agg}) + if err != nil { + t.Fatal("error getting proofs") + } + + proofMap := ProofsToMap(rawProofs) + + addresses, values, proofs, err := reporter.makeContractArgsWithProofs([]GlobalAggregate{agg}, proofMap) + if err != nil { + t.Fatal("error making contract args") + } + assert.Equal(t, reporter.SubmissionPairs[agg.Name].Address, addresses[0]) + assert.Equal(t, big.NewInt(15), values[0]) + + proofArr := make([][]byte, len(proofs)) + for i, p := range rawProofs { + proofArr[i] = p.Proof + } + + assert.EqualValues(t, proofs, proofArr) } func TestGetLatestGlobalAggregatesRdb(t *testing.T) { @@ -306,3 +328,61 @@ func TestGetLatestGlobalAggregatesPgsql(t *testing.T) { assert.Equal(t, result[0].Name, testItems.tmpData.globalAggregate.Name) assert.Equal(t, result[0].Value, testItems.tmpData.globalAggregate.Value) } + +func TestGetProofsRdb(t *testing.T) { + ctx := context.Background() + cleanup, testItems, err := setup(ctx) + if err != nil { + t.Fatalf("error setting up test: %v", err) + } + defer func() { + if cleanupErr := cleanup(); cleanupErr != nil { + t.Logf("Cleanup failed: %v", cleanupErr) + } + }() + + err = testItems.app.setReporters(ctx, testItems.app.Host, testItems.app.Pubsub) + if err != nil { + t.Fatalf("error setting reporters: %v", err) + } + reporter, err := testItems.app.GetReporterWithInterval(TestInterval) + if err != nil { + t.Fatalf("error getting reporter: %v", err) + } + + agg := testItems.tmpData.globalAggregate + result, err := reporter.getProofsRdb(ctx, []GlobalAggregate{agg}) + if err != nil { + t.Fatal("error getting proofs from rdb") + } + assert.EqualValues(t, testItems.tmpData.proofBytes, result[0].Proof) +} + +func TestGetProofsPgsql(t *testing.T) { + ctx := context.Background() + cleanup, testItems, err := setup(ctx) + if err != nil { + t.Fatalf("error setting up test: %v", err) + } + defer func() { + if cleanupErr := cleanup(); cleanupErr != nil { + t.Logf("Cleanup failed: %v", cleanupErr) + } + }() + + err = testItems.app.setReporters(ctx, testItems.app.Host, testItems.app.Pubsub) + if err != nil { + t.Fatalf("error setting reporters: %v", err) + } + reporter, err := testItems.app.GetReporterWithInterval(TestInterval) + if err != nil { + t.Fatalf("error getting reporter: %v", err) + } + + agg := testItems.tmpData.globalAggregate + result, err := reporter.getProofsPgsql(ctx, []GlobalAggregate{agg}) + if err != nil { + t.Fatal("error getting proofs from pgsql") + } + assert.EqualValues(t, testItems.tmpData.proofBytes, result[0].Proof) +} diff --git a/node/pkg/reporter/types.go b/node/pkg/reporter/types.go index ab3eaf349..d5cadd89d 100644 --- a/node/pkg/reporter/types.go +++ b/node/pkg/reporter/types.go @@ -21,7 +21,8 @@ const ( INITIAL_FAILURE_TIMEOUT = 50 * time.Millisecond MAX_RETRY = 3 MAX_RETRY_DELAY = 500 * time.Millisecond - FUNCTION_STRING = "submit(address[] memory _feeds, int256[] memory _submissions)" + SUBMIT_WITHOUT_PROOFS = "submit(address[] memory _feeds, int256[] memory _submissions)" + SUBMIT_WITH_PROOFS = "submit(address[] memory _feeds, int256[] memory _submissions, bytes[] memory _proofs)" GET_SUBMISSIONS_QUERY = `SELECT * FROM submission_addresses;` ) @@ -64,6 +65,19 @@ type GlobalAggregate struct { Round int64 `db:"round" json:"round"` } +type Proof struct { + Name string `json:"name"` + Round int64 `json:"round"` + Proof []byte `json:"proofs"` +} + +type PgsqlProof struct { + ID int64 `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Round int64 `db:"round" json:"round"` + Proof []byte `db:"proof" json:"proof"` +} + func makeGetLatestGlobalAggregatesQuery(names []string) string { queryNames := make([]string, len(names)) for i, name := range names { @@ -82,3 +96,12 @@ func makeGetLatestGlobalAggregatesQuery(names []string) string { return q } + +func makeGetProofsQuery(aggregates []GlobalAggregate) string { + placeHolders := make([]string, len(aggregates)) + for i, agg := range aggregates { + placeHolders[i] = fmt.Sprintf("('%s', %d)", agg.Name, agg.Round) + } + + return fmt.Sprintf("SELECT * FROM proofs WHERE (name, round) IN (%s);", strings.Join(placeHolders, ",")) +} diff --git a/node/script/test_submission/main.go b/node/script/test_submission/main.go index c628a25af..af485f365 100644 --- a/node/script/test_submission/main.go +++ b/node/script/test_submission/main.go @@ -2,61 +2,57 @@ package main import ( "context" - "fmt" "math/big" "bisonai.com/orakl/node/pkg/chain/helper" "github.com/rs/zerolog/log" ) -// send single submission through this script - -func testContractDelegatedCall(ctx context.Context, contractAddress string, contractFunction string, args ...interface{}) error { +func testContractDirectCall(ctx context.Context, contractAddress string, contractFunction string, args ...interface{}) error { klaytnHelper, err := helper.NewKlayHelper(ctx, "") if err != nil { log.Error().Err(err).Msg("NewTxHelper") return err } - rawTx, err := klaytnHelper.MakeFeeDelegatedTx(ctx, contractAddress, contractFunction, args...) + rawTx, err := klaytnHelper.MakeDirectTx(ctx, contractAddress, contractFunction, args...) if err != nil { - log.Error().Err(err).Msg("MakeFeeDelegatedTx") + log.Error().Err(err).Msg("MakeDirect") return err } - signedRawTx, err := klaytnHelper.GetSignedFromDelegator(rawTx) - if err != nil { - fmt.Println(signedRawTx) - log.Error().Err(err).Msg("GetSignedFromDelegator") - return err - } - - return klaytnHelper.SubmitRawTx(ctx, signedRawTx) + return klaytnHelper.SubmitRawTx(ctx, rawTx) } func main() { ctx := context.Background() - // contractAddress := flag.String("c", "0x93120927379723583c7a0dd2236fcb255e96949f", "contract address") - // contractFunction := flag.String("f", "increment()", "contract function") - - // flag.Parse() - // log.Info().Msgf("contractAddress: %s", *contractAddress) - // log.Info().Msgf("contractFunction: %s", *contractFunction) + s, err := helper.NewSignHelper("") + if err != nil { + log.Error().Err(err).Msg("NewSignHelper") - // err := testContractDelegatedCall(ctx, *contractAddress, *contractFunction) - // if err != nil { - // log.Error().Err(err).Msg("testContractDelegatedCall") - // } + } - // example code for dummy batch submission, check args usage from the code below + contractAddress := "0x08f43BebA1B0642C14493C70268a5AC8f380476b" + contractFunction := `test(int256 _answer, bytes memory _proof)` + answer := big.NewInt(200000000) + proof, err := s.MakeGlobalAggregateProof(200000000) + if err != nil { + log.Error().Err(err).Msg("MakeGlobalAggregateProof") + } + proofs := [][]byte{proof, proof} + testProof := concatBytes(proofs) - contractAddress := "0x8fb610c0Cc27Ca7726fad4c8696d09ca0E8eAee1" - contractFunction := `batchSubmit(string[] memory _pairs, int256[] memory _prices)` - pairs := []string{"BTC-USD", "ETH-USD"} - prices := []*big.Int{big.NewInt(100000000), big.NewInt(200000000)} - err := testContractDelegatedCall(ctx, contractAddress, contractFunction, pairs, prices) + err = testContractDirectCall(ctx, contractAddress, contractFunction, answer, testProof) if err != nil { - log.Error().Err(err).Msg("testContractDelegatedCall") + log.Error().Err(err).Msg("testContractDirectCall") + } +} + +func concatBytes(slices [][]byte) []byte { + var result []byte + for _, slice := range slices { + result = append(result, slice...) } + return result }