Skip to content

Commit

Permalink
Verify Tip Receipt content against logs (no cheating!) (#1898)
Browse files Browse the repository at this point in the history
updated the tip content to match the tip event
wrote new tests
updated the chain auth isWalletLinked to check the right wallet
  • Loading branch information
texuf authored Dec 20, 2024
1 parent e44fe85 commit fea5246
Show file tree
Hide file tree
Showing 12 changed files with 1,240 additions and 541 deletions.
493 changes: 493 additions & 0 deletions core/contracts/base/tipping.go

Large diffs are not rendered by default.

972 changes: 495 additions & 477 deletions core/node/protocol/protocol.pb.go

Large diffs are not rendered by default.

113 changes: 93 additions & 20 deletions core/node/rules/can_add_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"log/slog"
"math/big"
"slices"
"time"

Expand All @@ -13,6 +14,10 @@ import (

"github.com/ethereum/go-ethereum/common"

ethTypes "github.com/ethereum/go-ethereum/core/types"

baseContracts "github.com/river-build/river/core/contracts/base"

"github.com/river-build/river/core/node/auth"
. "github.com/river-build/river/core/node/base"
"github.com/river-build/river/core/node/dlog"
Expand Down Expand Up @@ -338,8 +343,8 @@ func (params *aeParams) canAddUserPayload(payload *StreamEvent_UserPayload) rule
return aeBuilder().
check(ru.params.creatorIsMember).
check(ru.validBlockchainTransaction_IsUnique).
check(ru.validBlockchainTransaction_ReceiptMetadata).
verifyReceipt(ru.blockchainTransaction_Receipt).
check(ru.validBlockchainTransaction_CheckReceiptMetadata).
verifyReceipt(ru.blockchainTransaction_GetReceipt).
requireChainAuth(ru.blockchainTransaction_ChainAuth).
requireParentEvent(ru.parentEventForBlockchainTransaction)
case *UserPayload_ReceivedBlockchainTransaction_:
Expand Down Expand Up @@ -616,13 +621,13 @@ func (ru *aeMemberBlockchainTransactionRules) validMemberBlockchainTransaction_R
if err != nil {
return false, err
}
err = checkIsMember(ru.params, content.Tip.GetToUserAddress())
err = checkIsMember(ru.params, content.Tip.GetReceiver())
if err != nil {
return false, err
}
// we need a ref event id
if content.Tip.GetRefEventId() == nil {
return false, RiverError(Err_INVALID_ARGUMENT, "tip transaction ref event id is nil")
if content.Tip.GetMessageId() == nil {
return false, RiverError(Err_INVALID_ARGUMENT, "tip transaction message id is nil")
}
return true, nil
default:
Expand Down Expand Up @@ -688,16 +693,67 @@ func (ru *aeBlockchainTransactionRules) validBlockchainTransaction_IsUnique() (b
return true, nil
}

func (ru *aeBlockchainTransactionRules) validBlockchainTransaction_ReceiptMetadata() (bool, error) {
func (ru *aeBlockchainTransactionRules) validBlockchainTransaction_CheckReceiptMetadata() (bool, error) {
receipt := ru.transaction.Receipt
if receipt == nil {
return false, RiverError(Err_INVALID_ARGUMENT, "receipt is nil")
}
// check creator
switch content := ru.transaction.Content.(type) {
case nil:
// for unspecified types, we don't need to check anything specific
// the other checks should make sure the transaction is valid and from this user
return true, nil
case *BlockchainTransaction_Tip_:
// todo
return true, nil
// parse the logs for the tip event, make sure it matches the tip metadata
filterer, err := baseContracts.NewTippingFilterer(common.Address{}, nil)
if err != nil {
return false, err
}
for _, receiptLog := range receipt.Logs {
// unpack the log
// compare to metadata in the tip
topics := make([]common.Hash, len(receiptLog.Topics))
for i, topic := range receiptLog.Topics {
topics[i] = common.BytesToHash(topic)
}
log := ethTypes.Log{
Address: common.BytesToAddress(receiptLog.Address),
Topics: topics,
Data: receiptLog.Data,
}
tipEvent, err := filterer.ParseTip(log)
if err != nil {
continue // not a tip
}
if tipEvent.TokenId.Cmp(big.NewInt(int64(content.Tip.GetTokenId()))) != 0 {
continue
}
if !bytes.Equal(tipEvent.Currency[:], content.Tip.GetCurrency()) {
continue
}
if !bytes.Equal(tipEvent.Sender[:], content.Tip.GetSender()) {
continue
}
if !bytes.Equal(tipEvent.Receiver[:], content.Tip.GetReceiver()) {
continue
}
if tipEvent.Amount.Cmp(big.NewInt(int64(content.Tip.GetAmount()))) != 0 {
continue
}
if !bytes.Equal(tipEvent.MessageId[:], content.Tip.GetMessageId()) {
continue
}
if !bytes.Equal(tipEvent.ChannelId[:], content.Tip.GetChannelId()) {
continue
}
// match found
return true, nil
}
return false, RiverError(
Err_INVALID_ARGUMENT,
"matching tip event not found in receipt logs",
)
default:
return false, RiverError(
Err_INVALID_ARGUMENT,
Expand All @@ -724,11 +780,11 @@ func (ru *aeReceivedBlockchainTransactionRules) parentEventForReceivedBlockchain
if !ok {
return nil, RiverError(Err_INVALID_ARGUMENT, "content is not a tip")
}
if content.Tip.GetStreamId() == nil {
return nil, RiverError(Err_INVALID_ARGUMENT, "transaction stream id is nil")
if content.Tip.GetChannelId() == nil {
return nil, RiverError(Err_INVALID_ARGUMENT, "transaction channel id is nil")
}
// convert to stream id
streamId, err := shared.StreamIdFromBytes(content.Tip.GetStreamId())
streamId, err := shared.StreamIdFromBytes(content.Tip.GetChannelId())
if err != nil {
return nil, err
}
Expand All @@ -752,11 +808,11 @@ func (ru *aeBlockchainTransactionRules) parentEventForBlockchainTransaction() (*
return nil, nil
case *BlockchainTransaction_Tip_:
// forward a "tip received" event to the user stream of the toUserAddress
userStreamId, err := shared.UserStreamIdFromBytes(content.Tip.GetToUserAddress())
userStreamId, err := shared.UserStreamIdFromBytes(content.Tip.GetReceiver())
if err != nil {
return nil, err
}
toStreamId, err := shared.StreamIdFromBytes(content.Tip.GetStreamId())
toStreamId, err := shared.StreamIdFromBytes(content.Tip.GetChannelId())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -789,20 +845,37 @@ func (ru *aeBlockchainTransactionRules) parentEventForBlockchainTransaction() (*
}
}

func (ru *aeBlockchainTransactionRules) blockchainTransaction_Receipt() (*BlockchainTransactionReceipt, error) {
return ru.transaction.Receipt, nil
func (ru *aeBlockchainTransactionRules) blockchainTransaction_GetReceipt() (*BlockchainTransactionReceipt, error) {
return ru.transaction.GetReceipt(), nil
}

// check to see that the transaction is from a wallet linked to the creator
func (ru *aeBlockchainTransactionRules) blockchainTransaction_ChainAuth() (*auth.ChainAuthArgs, error) {
if bytes.Equal(ru.transaction.Receipt.From, ru.params.parsedEvent.Event.CreatorAddress) {
return nil, nil
}
args := auth.NewChainAuthArgsForIsWalletLinked(
ru.params.parsedEvent.Event.CreatorAddress,
ru.transaction.Receipt.From,
)
return args, nil
switch content := ru.transaction.Content.(type) {
case nil:
// no content, verify the receipt.from
return auth.NewChainAuthArgsForIsWalletLinked(
ru.params.parsedEvent.Event.CreatorAddress,
ru.transaction.Receipt.From,
), nil
case *BlockchainTransaction_Tip_:
// tips can be sent through a bundler, verify the tip sender
// as specified in the tip content and verified against the logs in blockchainTransaction_CheckReceiptMetadata
return auth.NewChainAuthArgsForIsWalletLinked(
content.Tip.GetSender(),
ru.transaction.Receipt.From,
), nil
default:
return nil, RiverError(
Err_INVALID_ARGUMENT,
"unknown transaction type",
"transactionType",
content,
)
}
}

func (ru *aeMembershipRules) validMembershipPayload() (bool, error) {
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@ethereumjs/util": "^8.0.1",
"@river-build/dlog": "workspace:^",
"@river-build/encryption": "workspace:^",
"@river-build/generated": "workspace:^",
"@river-build/mls-rs-wasm": "^0.0.7",
"@river-build/proto": "workspace:^",
"@river-build/web3": "workspace:^",
Expand Down
19 changes: 9 additions & 10 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ import { SyncedStreamsExtension } from './syncedStreamsExtension'
import { SignerContext } from './signerContext'
import { decryptAESGCM, deriveKeyAndIV, encryptAESGCM, uint8ArrayToBase64 } from './crypto_utils'
import { makeTags } from './tags'
import { TipEventObject } from '@river-build/generated/dev/typings/ITipping'

export type ClientEvents = StreamEvents & DecryptionEvents

Expand Down Expand Up @@ -1932,20 +1933,18 @@ export class Client
async addTransaction_Tip(
chainId: number,
receipt: ContractReceipt,
streamId: string | Uint8Array,
refEventId: string,
toUserId: string,
quantity: bigint,
currency: string,
event: TipEventObject,
): Promise<{ eventId: string }> {
return this.addTransaction(chainId, receipt, {
case: 'tip',
value: {
streamId: streamIdAsBytes(streamId),
refEventId: bin_fromHexString(refEventId),
toUserAddress: addressFromUserId(toUserId),
quantity: quantity,
currency: bin_fromHexString(currency),
tokenId: event.tokenId.toBigInt(),
currency: bin_fromHexString(event.currency),
sender: addressFromUserId(event.sender),
receiver: addressFromUserId(event.receiver),
amount: event.amount.toBigInt(),
messageId: bin_fromHexString(event.messageId),
channelId: streamIdAsBytes(event.channelId),
},
})
}
Expand Down
10 changes: 5 additions & 5 deletions packages/sdk/src/sync-agent/timeline/models/timelineEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -988,11 +988,11 @@ function getFallbackContent_BlockchainTransaction(
}
switch (transaction.content.case) {
case 'tip':
return `kind: ${transaction.content.case} refEventId: ${bin_toHexString(
transaction.content.value.refEventId,
)} toUserAddress: ${bin_toHexString(
transaction.content.value.toUserAddress,
)} quantity: ${transaction.content.value.quantity.toString()}`
return `kind: ${transaction.content.case} messageId: ${bin_toHexString(
transaction.content.value.messageId,
)} receiver: ${bin_toHexString(
transaction.content.value.receiver,
)} amount: ${transaction.content.value.amount.toString()}`
default:
return `kind: ${transaction.content.case ?? 'unspecified'}`
}
Expand Down
Loading

0 comments on commit fea5246

Please sign in to comment.