Skip to content

Commit

Permalink
Tip events, properly handle the to user address (#1903)
Browse files Browse the repository at this point in the history
The sender and receiver of the tip are the wallets that hold the space
nfts, not the root wallet addresses. So we can’t assume that the
receiver will have a user stream. We need to specify the to user address
in the metadata and verify on appending a received transaction that this
actually belongs to the user.
  • Loading branch information
texuf authored Dec 28, 2024
1 parent f1effe5 commit 702fe28
Show file tree
Hide file tree
Showing 7 changed files with 747 additions and 572 deletions.
4 changes: 2 additions & 2 deletions core/node/auth/auth_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ func NewChainAuthArgsForIsSpaceMember(spaceId shared.StreamId, userId string) *C
}

func NewChainAuthArgsForIsWalletLinked(
userId []byte,
userAddress []byte,
walletAddress []byte,
) *ChainAuthArgs {
return &ChainAuthArgs{
kind: chainAuthKindIsWalletLinked,
principal: common.BytesToAddress(userId),
principal: common.BytesToAddress(userAddress),
walletAddress: common.BytesToAddress(walletAddress),
}
}
Expand Down
1,135 changes: 606 additions & 529 deletions core/node/protocol/protocol.pb.go

Large diffs are not rendered by default.

56 changes: 42 additions & 14 deletions core/node/rules/can_add_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ func (params *aeParams) canAddUserPayload(payload *StreamEvent_UserPayload) rule
return aeBuilder().
check(ru.params.creatorIsValidNode).
check(ru.validReceivedBlockchainTransaction_IsUnique).
requireChainAuth(ru.receivedBlockchainTransaction_ChainAuth).
requireParentEvent(ru.parentEventForReceivedBlockchainTransaction)
default:
return aeBuilder().
Expand Down Expand Up @@ -621,12 +622,12 @@ func (ru *aeMemberBlockchainTransactionRules) validMemberBlockchainTransaction_R
if err != nil {
return false, err
}
err = checkIsMember(ru.params, content.Tip.GetReceiver())
err = checkIsMember(ru.params, content.Tip.GetToUserAddress())
if err != nil {
return false, err
}
// we need a ref event id
if content.Tip.GetMessageId() == nil {
if content.Tip.GetEvent().GetMessageId() == nil {
return false, RiverError(Err_INVALID_ARGUMENT, "tip transaction message id is nil")
}
return true, nil
Expand Down Expand Up @@ -726,25 +727,25 @@ func (ru *aeBlockchainTransactionRules) validBlockchainTransaction_CheckReceiptM
if err != nil {
continue // not a tip
}
if tipEvent.TokenId.Cmp(big.NewInt(int64(content.Tip.GetTokenId()))) != 0 {
if tipEvent.TokenId.Cmp(big.NewInt(int64(content.Tip.GetEvent().GetTokenId()))) != 0 {
continue
}
if !bytes.Equal(tipEvent.Currency[:], content.Tip.GetCurrency()) {
if !bytes.Equal(tipEvent.Currency[:], content.Tip.GetEvent().GetCurrency()) {
continue
}
if !bytes.Equal(tipEvent.Sender[:], content.Tip.GetSender()) {
if !bytes.Equal(tipEvent.Sender[:], content.Tip.GetEvent().GetSender()) {
continue
}
if !bytes.Equal(tipEvent.Receiver[:], content.Tip.GetReceiver()) {
if !bytes.Equal(tipEvent.Receiver[:], content.Tip.GetEvent().GetReceiver()) {
continue
}
if tipEvent.Amount.Cmp(big.NewInt(int64(content.Tip.GetAmount()))) != 0 {
if tipEvent.Amount.Cmp(big.NewInt(int64(content.Tip.GetEvent().GetAmount()))) != 0 {
continue
}
if !bytes.Equal(tipEvent.MessageId[:], content.Tip.GetMessageId()) {
if !bytes.Equal(tipEvent.MessageId[:], content.Tip.GetEvent().GetMessageId()) {
continue
}
if !bytes.Equal(tipEvent.ChannelId[:], content.Tip.GetChannelId()) {
if !bytes.Equal(tipEvent.ChannelId[:], content.Tip.GetEvent().GetChannelId()) {
continue
}
// match found
Expand All @@ -764,6 +765,33 @@ func (ru *aeBlockchainTransactionRules) validBlockchainTransaction_CheckReceiptM
}
}

func (ru *aeReceivedBlockchainTransactionRules) receivedBlockchainTransaction_ChainAuth() (*auth.ChainAuthArgs, error) {
transaction := ru.receivedTransaction.Transaction
if transaction == nil {
return nil, RiverError(Err_INVALID_ARGUMENT, "transaction is nil")
}

switch content := transaction.Content.(type) {
case nil:
return nil, nil
case *BlockchainTransaction_Tip_:
userAddress, err := shared.GetUserAddressFromStreamId(*ru.params.streamView.StreamId())
if err != nil {
return nil, err
}
if !bytes.Equal(content.Tip.GetToUserAddress(), userAddress.Bytes()) {
return nil, RiverError(Err_INVALID_ARGUMENT, "to user address is not the user", "toUser", content.Tip.GetToUserAddress(), "user", userAddress.Bytes())
}
// make sure that the receiver (in the event emitted from the tipping facet) is one of our wallets
return auth.NewChainAuthArgsForIsWalletLinked(
userAddress.Bytes(),
content.Tip.GetEvent().GetReceiver(),
), nil
default:
return nil, RiverError(Err_INVALID_ARGUMENT, "unknown received transaction kind for chain auth", "kind", ru.receivedTransaction.Kind)
}
}

func (ru *aeReceivedBlockchainTransactionRules) parentEventForReceivedBlockchainTransaction() (*DerivedEvent, error) {
transaction := ru.receivedTransaction.Transaction
if transaction == nil {
Expand All @@ -780,11 +808,11 @@ func (ru *aeReceivedBlockchainTransactionRules) parentEventForReceivedBlockchain
if !ok {
return nil, RiverError(Err_INVALID_ARGUMENT, "content is not a tip")
}
if content.Tip.GetChannelId() == nil {
if content.Tip.GetEvent().GetChannelId() == nil {
return nil, RiverError(Err_INVALID_ARGUMENT, "transaction channel id is nil")
}
// convert to stream id
streamId, err := shared.StreamIdFromBytes(content.Tip.GetChannelId())
streamId, err := shared.StreamIdFromBytes(content.Tip.GetEvent().GetChannelId())
if err != nil {
return nil, err
}
Expand All @@ -808,11 +836,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.GetReceiver())
userStreamId, err := shared.UserStreamIdFromBytes(content.Tip.GetToUserAddress())
if err != nil {
return nil, err
}
toStreamId, err := shared.StreamIdFromBytes(content.Tip.GetChannelId())
toStreamId, err := shared.StreamIdFromBytes(content.Tip.GetEvent().GetChannelId())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -866,7 +894,7 @@ func (ru *aeBlockchainTransactionRules) blockchainTransaction_ChainAuth() (*auth
// as specified in the tip content and verified against the logs in blockchainTransaction_CheckReceiptMetadata
return auth.NewChainAuthArgsForIsWalletLinked(
ru.params.parsedEvent.Event.CreatorAddress,
content.Tip.GetSender(),
content.Tip.GetEvent().GetSender(),
), nil
default:
return nil, RiverError(
Expand Down
18 changes: 11 additions & 7 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1934,17 +1934,21 @@ export class Client
chainId: number,
receipt: ContractReceipt,
event: TipEventObject,
toUserId: string,
): Promise<{ eventId: string }> {
return this.addTransaction(chainId, receipt, {
case: 'tip',
value: {
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),
event: {
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),
},
toUserAddress: addressFromUserId(toUserId),
},
})
}
Expand Down
9 changes: 6 additions & 3 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,14 @@ function getFallbackContent_BlockchainTransaction(
}
switch (transaction.content.case) {
case 'tip':
if (!transaction.content.value?.event) {
return '??'
}
return `kind: ${transaction.content.case} messageId: ${bin_toHexString(
transaction.content.value.messageId,
transaction.content.value.event.messageId,
)} receiver: ${bin_toHexString(
transaction.content.value.receiver,
)} amount: ${transaction.content.value.amount.toString()}`
transaction.content.value.event.receiver,
)} amount: ${transaction.content.value.event.amount.toString()}`
default:
return `kind: ${transaction.content.case ?? 'unspecified'}`
}
Expand Down
76 changes: 68 additions & 8 deletions packages/sdk/src/tests/multi/transactions_Tip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ const base_log = dlog('csb:test:transactions_Tip')
describe('transactions_Tip', () => {
const riverConfig = makeRiverConfig()
const bobIdentity = new Bot(undefined, riverConfig)
const aliceIdentity = new Bot(undefined, riverConfig)
const bobsOtherWallet = ethers.Wallet.createRandom()
const bobsOtherWalletProvider = new LocalhostWeb3Provider(
riverConfig.base.rpcUrl,
bobsOtherWallet,
)
const aliceIdentity = new Bot(undefined, riverConfig)
const alicesOtherWallet = ethers.Wallet.createRandom()
const chainId = riverConfig.base.chainConfig.chainId

// updated once and shared between tests
Expand Down Expand Up @@ -64,6 +65,10 @@ describe('transactions_Tip', () => {
bobIdentity.signer,
bobsOtherWallet,
),
alice.riverConnection.spaceDapp.walletLink.linkWalletToRootKey(
aliceIdentity.signer,
alicesOtherWallet,
),
])

// before they can do anything on river, they need to be in a space
Expand Down Expand Up @@ -137,7 +142,12 @@ describe('transactions_Tip', () => {
)
expect(tipEvent).toBeDefined()
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, receipt, tipEvent!),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
receipt,
tipEvent!,
aliceIdentity.rootWallet.address,
),
).resolves.not.toThrow()
})

Expand Down Expand Up @@ -213,39 +223,89 @@ describe('transactions_Tip', () => {
const event = cloneDeep(dummyTipEvent)
event.channelId = makeUniqueChannelStreamId(spaceId)
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadMessageId', async () => {
const event = cloneDeep(dummyTipEvent)
event.messageId = randomBytes(32).toString('hex')
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadFromUserAddress', async () => {
test('cantAddTipWithBadSender', async () => {
const event = cloneDeep(dummyTipEvent)
event.sender = aliceIdentity.rootWallet.address
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadReceiver', async () => {
const event = cloneDeep(dummyTipEvent)
event.receiver = bobIdentity.rootWallet.address
await expect(
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadAmount', async () => {
const event = cloneDeep(dummyTipEvent)
event.amount = BigNumber.from(10000000n)
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadCurrency', async () => {
const event = cloneDeep(dummyTipEvent)
event.currency = '0x0000000000000000000000000000000000000000'
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadToUserAddress', async () => {
const event = cloneDeep(dummyTipEvent)
await expect(
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
bobIdentity.rootWallet.address,
),
).rejects.toThrow('IsEntitled failed')
})
})
21 changes: 12 additions & 9 deletions protocol/protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -611,19 +611,22 @@ message Snapshot {
}

message BlockchainTransaction {
// metadata for tip transactions
message Tip {
uint64 token_id = 1;
bytes currency = 2;
bytes sender = 3;
bytes receiver = 4;
uint64 amount = 5;
bytes message_id = 6;
bytes channel_id = 7;
message Event {
uint64 token_id = 1;
bytes currency = 2;
bytes sender = 3; // wallet that sent funds
bytes receiver = 4; // wallet that received funds
uint64 amount = 5;
bytes message_id = 6;
bytes channel_id = 7;
}
Event event = 1; // event emitted by the tipping facet
bytes toUserAddress = 2; // user that received funds
}

// required fields
BlockchainTransactionReceipt receipt = 1;

// optional metadata to be verified by the node
oneof content {
Tip tip = 101;
Expand Down

0 comments on commit 702fe28

Please sign in to comment.