Skip to content

Conversation

@benbierens
Copy link
Contributor

From the Deceptive-contracts tests in the Archivist release test repo (https://github.com/durability-labs/cs-archivist-dist-tests/blob/main/Tests/ArchivistReleaseTests/MarketTests/DeceptiveContractTest.cs) we have the following behavior:

Storage requests posted on-chain contain a slotSize number (representing bytes in a single slot).
This is used by hosts to determine if they have capacity for the slot.
This is used by the marketplace SmartContract to determine the price payout to hosts.
The on-chain request also contains a CID (representing a verifiable-manifest).
A verifiable manifest also defines a slotSize in bytes.

In the current implementation of the node, the slotSize posted on-chain and the slotSize of the veriable manifest are allowed to mismatch. This can be exploited easily. Posting a contract on-chain with a small slotSize, but providing a CID with a large slotSize can trick a host into storing a large slot while receiving payment for a small one. Additionally, it can screw with a host's quota management.

This PR fixes the issue by:
As soon as a storage request is picked up and the manifest is fetched, we check that the manifest slotSize matches the storageRequest slotSize. If it does not, we do not proceed to download and fail out of the sales statemachine.

Sidenote:
It's possible to over-report your slotSize on-chain. It would be of benefit to hosts to pick up these contracts. You get paid more for doing less work! But, this PR expects the storage request to be honest: The numbers must match exactly.

@benbierens benbierens requested review from dryajov and markspanbroek and removed request for markspanbroek January 16, 2026 09:18
Copy link
Contributor

@markspanbroek markspanbroek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix @benbierens! I left a few comments.

Comment on lines +16 to +27
type StoreSlotAsk* = object
cid*: Cid
slotIndex*: uint64
slotSize*: uint64
expiry*: SecondsSince1970
repair*: bool

method available*(storage: StorageInterface): uint64 {.base, gcsafe, raises: [].} =
raiseAssert "not implemented"

method storeSlot*(
storage: StorageInterface,
cid: Cid,
slotIndex: uint64,
expiry: SecondsSince1970,
repair: bool,
storage: StorageInterface, storeAsk: StoreSlotAsk
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why you extracted a parameter object here, the amount of parameters is quite a lot. But for me, this doesn't make the code better to read. It immediately raises questions: what is a StoreSlotAsk, what's the difference with a StorageAsk, why is a repair boolean in the same object as slot properties?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not too fond of the name 'StoreSlotAsk', but I think this object should exist. These parameters travel together through several signatures. They represent a concept... something like: "Everything you need to store this slot". But it's a node-type, independent of the smartcontract types.
It's also nice to know that this object is constructed in one place only, so you know where the data comes from.


type SalesData* = ref object
requestId*: RequestId
ask*: StorageAsk
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to ask you (pun intended) to keep this property in. I know it's currently not used, but it should be. One of the first things we do in the sales state machine is get the entire request, but we can already do a lot with just the ask, which we get for free with the StorageRequested event.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property is never assigned to, nor is it used. Its only purpose seems to be, to confuse and annoy me while I was implementing this fix. That purpose is now fulfilled, so let's remove it.

In the future, I think we should definitely clean up the optional-request field as you suggest. At that time, something like this can be re-introduced. Until that time, let's not accidentally waste anyone's time on fields that may some day be used.

repair: false,
)
let response = await storage.storeSlot(ask)
check response.isFailure
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to check the error message as well. I've seen tests deteriorate over time because they should be checking for a certain error, but accidentally keep succeeding because there's another error being returned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants